其中, jemalloc 性能最高, tcmalloc 次之, ptmalloc 性能最差。
系统的物理内存是有限的, 而对内存的需求是变化的, 程序的动态性越强, 内存管理就越重要, 选择合适的内存管理算法会带来明显的性能提升。
比如 nginx, 它在每个连接 accept 后会 malloc 一块内存, 作为整个连接生命周期内的内存池。当 HTTP 请求到达的时候, 又会 malloc 一块当前请求阶段的内存池, 因此对 malloc 的分配速度有一定的依赖关系。(而 apache 的内存池是有父子关系的, 请求阶段的内存池会和连接阶段的使用相同的分配器, 如果连接内存池释放则请求阶段的子内存池也会自动释放)。
内存管理可以分为三个层次, 自底向上分别是:
std::shared_ptr
, apache 的内存池方式等等。当然应用程序也可以直接使用系统调用从内核分配内存, 自己根据程序特性来维护内存, 但是会大大增加开发成本。
本文主要介绍了 glibc malloc 的实现, 及其替代品
一个优秀的通用内存分配器应具有以下特性:
目前大部分服务端程序使用 glibc 提供的 malloc/free 系列函数, 而 glibc 使用的 ptmalloc2 在性能上远远弱后于 google 的 tcmalloc 和 facebook 的 jemalloc。 而且后两者只需要使用 LD_PRELOAD
环境变量启动程序即可, 甚至并不需要重新编译。
ptmalloc2 即是我们当前使用的 glibc malloc 版本。
上图是 x86_64 下 Linux 进程的默认地址空间, 对 heap 的操作, 操作系统提供了 brk() 系统调用, 设置了 Heap 的上边界; 对 mmap 映射区域的操作, 操作系 统 供了 mmap() 和 munmap() 函数。
因为系统调用的代价很高, 不可能每次申请内存都从内核分配空间, 尤其是对于小内存分配。 而且因为 mmap 的区域容易被 munmap 释放, 所以一般大内存采用 mmap(), 小内存使用 brk()。
Ptmalloc2 有一个主分配区 (main arena), 有多个非主分配区。 非主分配区只能使用 mmap 向操作系统批发申请 HEAP_MAX_SIZE(64 位系统为 64MB) 大小的虚拟内存。 当某个线程调用 malloc 的时候, 会先查看线程私有变量中是否已经存在一个分配区, 如果存在则尝试加锁, 如果加锁失败则遍历 arena 链表试图获取一个没加锁的 arena, 如果依然获取不到则创建一个新的非主分配区。
free()
的时候也要获取锁。分配小块内存容易产生碎片, ptmalloc 在整理合并的时候也要对 arena 做加锁操作。在线程多的时候, 锁的开销就会增大。
用户请求分配的内存在 ptmalloc 中使用 chunk 表示, 每个 chunk 至少需要 8 个字节额外的开销。 用户 free 掉的内存不会马上归还操作系统, ptmalloc 会统一管理 heap 和 mmap 区域的空闲 chunk, 避免了频繁的系统调用。
ptmalloc 将相似大小的 chunk 用双向链表链接起来, 这样的一个链表被称为一个 bin。Ptmalloc 一共 维护了 128 个 bin, 并使用一个数组来存储这些 bin(如下图所示)。
ptmalloc 结构图
数组中的第一个为 unsorted bin, 数组中从 2 开始编号的前 64 个 bin 称为 small bins, 同一个 small bin 中的 chunk 具有相同的大小。small bins 后面的 bin 被称作 large bins。
当 free 一个 chunk 并放入 bin 的时候, ptmalloc 还会检查它前后的 chunk 是否也是空闲的, 如果是的话, ptmalloc 会首先把它们合并为一个大的 chunk, 然后将合并后的 chunk 放到 unstored bin 中。 另外 ptmalloc 为了提高分配的速度, 会把一些小的 (不大于 64B) chunk 先放到一个叫做 fast bins 的容器内。
在 fast bins 和 bins 都不能满足需求后, ptmalloc 会设法在一个叫做 top chunk 的空间分配内存。 对于非主分配区会预先通过 mmap 分配一大块内存作为 top chunk, 当 bins 和 fast bins 都不能满足分配需要的时候, ptmalloc 会设法在 top chunk 中分出一块内存给用户, 如果 top chunk 本身不够大, 分配程序会重新 mmap 分配一块内存 chunk, 并将 top chunk 迁移到新的 chunk 上, 并用单链表链接起来。如果 free() 的 chunk 恰好 与 top chunk 相邻, 那么这两个 chunk 就会合并成新的 top chunk, 如果 top chunk 大小大于某个阈值才还给操作系统。主分配区类似, 不过通过 sbrk() 分配和调整 top chunk 的大小, 只有 heap 顶部连续内存空闲超过阈值的时候才能回收内存。
需要分配的 chunk 足够大, 而且 fast bins 和 bins 都不能满足要求, 甚至 top chunk 本身也不能满足分配需求时, ptmalloc 会使用 mmap 来直接使用内存映射来将页映射到进程空间。
tcmalloc 是 Google 开源的一个内存管理库, 作为 glibc malloc 的替代品。目前已经在 chrome、safari 等知名软件中运用。
根据官方测试报告, ptmalloc 在一台 2.8GHz 的 P4 机器上 (对于小对象) 执行一次 malloc 及 free 大约需要 300 纳秒。而 TCMalloc 的版本同样的操作大约只需要 50 纳秒。
测试环境是 2.4GHz dual Xeon, 开启超线程, redhat9, glibc-2.3.2, 每个线程测试 100 万个操作。
上图中可以看到尤其是对于小内存的分配, tcmalloc 有非常明显性能优势。
上图可以看到随着线程数的增加, tcmalloc 性能上也有明显的优势, 并且相对平稳。
github 使用 tcmalloc 后, mysql 性能提升 30%
jemalloc 是 facebook 推出的, 最早的时候是 freebsd 的 libc malloc 实现。 目前在 firefox、facebook 服务器各种组件中大量使用。
上图可以看到每个 arena 管理的 arena chunk 结构, 开始的 header 主要是维护了一个 page map(1024 个页面关联的对象状态), header 下方就是它的页面空间。 Small 对象被分到一起, metadata 信息存放在起始位置。 large chunk 相互独立, 它的 metadata 信息存放在 chunk header map 中。
上图是服务器吞吐量分别用 6 个 malloc 实现的对比数据, 可以看到 tcmalloc 和 jemalloc 最好 (facebook 在 2011 年的测试结果, tcmalloc 这里版本较旧)。
测试环境: 2x Intel E5/2.2Ghz with 8 real cores per socket, 16 real cores, 开启 hyper-threading, 总共 32 个 vcpu。 16 个 table, 每个 5M row。
OLTP_RO 测试包含 5 个 select 查询: select_ranges, select_order_ranges, select_distinct_ranges, select_sum_ranges,
可以看到在多核心或者多线程的场景下, jemalloc 和 tcmalloc 带来的 tps 增加非常明显。
在多线程环境使用 tcmalloc 和 jemalloc 效果非常明显。
当线程数量固定, 不会频繁创建退出的时候, 可以使用 jemalloc; 反之使用 tcmalloc 可能是更好的选择。
今年年初由于 facebook 而火起来的 jemalloc 广为人之, 但殊不知, 它在 malloc 界里面很早就出名了。Jemalloc 的创始人 Jason Evans 也是在 FreeBSD 很有名的开发人员。此人就在 2006 年为提高低性能的 malloc 而写的 jemalloc。Jemalloc 是从 2007 年开始以 FreeBSD 标准引进来的。软件技术革新很多是 FreeBSD 发起的。在 FreeBSD 应用广泛的技术会慢慢导入到 linux。
目前 jemalloc 在 firefox 中也在使用。在 firefox2 中出现了内存碎片问题之后, 便在 firefox3 中使用了 jemalloc。在 safari 和 chrome 中使用的是 google 的 tcmalloc。
Jemalloc 聚集了 malloc 的使用过程中所验证的很多技术。忽略细节, 从架构着眼, 最出色的部分仍是 arena 和 thread cache。(事实上, 这两个与 tcmalloc 的架构几乎相同。Jemalloc only 的部分将会在另一次 posting 中继续探讨。)
与其像 malloc 一样集中管理一整块内存, 不如将其分成许多个小块来分而治之。此小块便称为 arena。让我们想象一下, 给几个小朋友一张大图纸, 让他们随意地画点。结果可想而知, 他们肯定相互顾忌对方而不敢肆意地画(synchronization), 从而影响画图效率。但是如果老师事先在大图纸上划分好每个人的区域, 小朋友们就可以又快又准地在各自地领域上画图。这样的概念就是 arena。
如果是开辟小块内存, 为使不参照 arena 而直接 malloc, 给各自的线程 thread cache 领域。此 idea 是 google 的 tcmalloc 的核心部分, 亦在 jemalloc 中体现。
再拿上面的例子, 这次给小朋友们除了一张大图纸外, 再各自给 A4 纸一张。这样, 小朋友们在不画大面积的点时, 只在自己的 A4 纸上心情地画即可(no arena seeking)。可以在自己手上的纸上画或涂(using thread cache), 完全不用顾忌别人(no synchronization, no locking), 迅速有效地画。
下图是 jemalloc 的核心 layout。看着复杂, 其实都是上面说明的部分。
最左边的就是 glibc 的 malloc, 最右边的就是 jemalloc。从图表上可以看出, jemalloc 的性能有 glibc 的两倍以上。非常压倒性的性能差异。因此, 使用了 jemalloc 的应用程序自然会快很多。Jemalloc 旁边的就是 tcmalloc。Tcmalloc 的性能与其相差甚微, 低 jemalloc2.1.0 慢 4.5%。图上和 tcmalloc 的 1.4 版本, 而如今它已经到了 1.6 版本, 因此实际上这两者应该是不相仲伯的。Jemalloc 的创始人 jason evans 也意识到这一点, 说在 cpu core 8 以上的计算机上 jemalloc 效率更高。
2005 年发表了一篇文章 “免费午餐的时代结束了”。在之前, 程序就算不用费脑子, 随着 cpu 时钟速度增加, 程序性能自己就会上去。但现在不同, 现在 cpu 时钟趋于稳定, 而核数不断地增加。程序员需要适应这样的多线程多进程的环境, 并要开发出适合的程序。文章讲的大概是这样的内容。
6 年之后的如今, 这篇文章完全变成现实了。事实上 cpu 时钟停留在 3GHz, 而核不断上升。现在程序要适应多线程多进程的分布式计算, 速度才能上升。但是这样的程序很难。
现在在多线程的环境下, 给程序员们的最后一道午餐便是 tcmalloc, jemalloc 这样的 malloc library。对于使用多线程的程序而言, 性能会提高数十 %。
共享一下我本人的经验。我本人在 kth 技术研究所分布式技术 lab 中承担 iLock(分布式同步工具, 请参考 google 的 chubby)。在 iLock 中用了 google 的 tcmalloc 的结果, 性能提升了 18~22%。
最大的优点就是你不需要做任何复杂的工作便可得到这样的效果。不需要代码重编译。只需在执行二进制之前, 在 cmd 窗口中输入
$ LD_PRELOAD="tcmalloc 所设置的文件夹 / libtcmalloc.so"
这样在之后执行的应用程序会使用 tcmalloc 或 jemalloc, 从而代替 glibc 标准 malloc(ptmalloc)。这需设置此处, 我们便可得到性能 20% 的提升, 这真可谓是送给我们的最后的免费午餐。
如今, 在分布式技术 lab 中使用 google 的 tcmalloc。原因在于性能上两者差不多, 但 google 的 tcmalloc 所提供的程序分析工具非常 (heap profiler, cpu profiler) 丰富。所以 tcmalloc 可能更方便一些。
一定要使用最新的 malloc 么? 一定要的!