就在近日,Facebook宣布开源了内部使用的C++底层库,总称folly,包括散列、字符串、向量、内存分配、位处理等,以满足大规模高性能的需求。
这里是folly的github地址:https://github.com/facebook/folly
在folly项目的Overview.md中,谈到了folly库的初衷:
It complements (as opposed to competing against) offerings such as Boost and of course
std
. In fact, we embark on defining our own component only when something we need is either not available, or does not meet the needed performance profile.
除了小部分是对现有标准库和Boost库功能上的补充,大部分都是基于性能的需求而“重新制造轮子”。
特别是大规模下的性能需求,大规模下的性能追求是Folly统一的主题:
Good performance at large scale is a unifying theme in all of Folly.
一是因为string几乎是C++程序中最常用的“容器”,性能至关重要;
二是因为之前也曾写过一篇博客《std::string的Copy-on-Write:不如想象中美好》,研究了std::string的copy-on-write实现的优缺点,因此想要看看Facebook究竟需要什么样的string。
folly自定义的string(以下简称为fbstring)的核心实现位于 folly/FBString.h。
还有一些fbstring的辅助函数(如向std::string的转换、各种格式的输出、escape、demangle等),位于 folly/String.h 和folly/String.cpp ,由于本文主要谈的是fbstring的内部实现,这些内容暂且不提,有兴趣的童鞋可以自己参考源码,folly的代码还是写得相当漂亮的:)
folly对string类的设计和优化,主要体现在两个方面:
1. 内存模型
2. 常用方法的优化
下面将逐一说明。
1. 内存布局及策略
fbstring使用了三层的存储策略(three-tiered storage strategy),根据长度将fbstring分为三类:small/medium/large,分别采取不同的优化措施,以达到最佳性能。
fbstring内存模型示意图(使用LucidChart绘制):
简单来说:
短string:直接放在(栈上)对象中,避免了动态内存分配的开销。结构体长度为24字节,减去末尾的1字节(用来表示长度)和为结束符'\0'(data()和c_str()方法的需要)预留的1字节,可以放置22字节的有效长度。
中等string(小于255字节):直接通过malloc分配,并且采用eager-copy的方式,即字符串的复制总是会重新分配并拷贝内容。
至于为什么不用copy-on-write:
1. 我之前的博客也提到,copy-on-write的额外开销(原子操作、容易失效)一定程度上抵消了减少一次内存分配和拷贝带来的好处
2. folly鼓励使用jemalloc来代替glibc下默认的ptmalloc2,并且在代码中迎合jemalloc的使用做了大量优化。在这里,分配一个小片内存区域的开销是极小的,下文还会有说明。
较长string(大于255字节):使用copy-on-write,减少分配和拷贝大内存的开销。在这里,folly使用了C++11中的原子变量:std::atomic
PS:使用capacity最高位的4个bits来判断string的种类,folly假定机器的字节序为小端(little endian),适用于x86-64平台上的大部分OS。
2. 内存分配器
与std::string不同,fbstring并没有从模板参数之一的Allocator获取内存,而是直接使用malloc/free管理内存。
fbstring推荐使用jemalloc而不是Linux下glibc默认的ptmalloc2来管理动态内存:
1. 作为FreeBSD上的默认分配器,jemalloc在多线程并发的环境下表现更好(与google开源的tcmalloc性能相近)。
在tcmalloc的论文《TCMalloc : Thread-Caching Malloc》中,提到了ptmalloc2在多线程环境下的一个致命缺陷:
ptmalloc2同样通过为不同的线程分配自己的内存池(Arena)的方式来减少并发分配时的锁冲突,但ptmalloc2中线程拥有的内存池是不能迁移的,在某些情况下能够带来巨大的内存浪费:比如一个线程在开始阶段分配了300MB的内存进行初始化工作,然后释放了,但接下来的线程分配到不同的内存池,那么之前的300MB是无法重复利用的。
2. folly如果检测到使用jemalloc,那么将使用jemalloc的一些非标准扩展接口来提高性能。
PS:folly通过定义弱符号(weak symbol)的方法来运行时判断是否使用了jemalloc:
extern "C" int rallocm(void**, size_t*, size_t, size_t, int) __attribute__((weak)); /** * Determine if we are using jemalloc or not. */ inline bool usingJEMalloc() { return rallocm != NULL; }
如果使用了jemalloc,一个典型的优化是使用jemalloc特有的rallocm来代替标准的realloc方法。(下面还会提到realloc的优化)
同时,所有动态内存请求的大小都会经过一个过滤函数:goodMallocSize(在folly/Malloc.h中)处理,以获取一个对jemalloc友好的值
goodMallocSize在不同的请求区间,将请求大小设置为64b / 256b / 4KB / 4MB对齐,以提高分配/回收效率,减少内存碎片。
fbstring在实现时做了很多优化(如word-wise copy等),其中的细节不再一一敷述,感兴趣的读者建议去参考源码,这里只列出重要的几点:
1. 末尾'\0'的处理
fbstring的默认行为是“懒惰”添加'\0'(lazy append),即平时预留空间,只在调用data()或者c_str()时,才在结尾添加'\0',避免了每次修改字符串时的额外开销(特别是push_back操作),因为这样做是符合C++标准的。
(当然,fbstring也有相应的宏来关闭该行为)
2. realloc的处理
string很多时候需要realloc,为了优化realloc的效率,fbstring做了这样的设定:
(1)如果使用jemalloc:使用jemalloc的非标准接口——rallocm
(2)没有使用jemalloc:
当前内存的使用率小于50%(size * 2 < capacity),放弃使用realloc(因为realloc可能需要拷贝全部内存,而其中超过一半是无效内容),而是简单采用free+malloc+copy的方式来重新分配内存,减少拷贝开销。
当前内存的使用率大于50%,则使用realloc,寄希望realloc可以合并后面的内存(coalescing)以避免拷贝。
3. 优化string::find()
glibc的string::find()实现中只实现了简单的逐字符查找比较功能,复杂度为O(M*N)。(C++标准并没有规定string::find的复杂度要求)
find使用了简化的Boyer-Moore算法,代码中声称:
Casual tests indicate a 30x speed improvement over
string::find()
for successful searches and a 1.5xspeed improvement for failed searches.
如果是简单的短字符查询,string::find()应该足够高效。只有在长字符搜索的情况下,find的BM算法实现才能体现出优势,或许这也是Facebook的常用场景吧。
顺便提一下,fbstring(FBString.h)的作者为Andrei Alexandrescu(熟悉C++应该都听说过),近距离欣赏大师的代码实在是一种享受。
同时,Alexandre大叔以43岁的“高龄”,依然在Facebook写着如此底层的程序。个中滋味,值得天朝所有浮躁的程序员(包括笔者在内)和“35岁论“者细细体味。
PromisE_谢 (Xielingjun6522@163.com) 原创文章
转载请注明出处:http://www.cnblogs.com/promise6522/archive/2012/06/05/2535530.html