土法炼钢兴趣小组的算法知识备份

内存分配器擂台:jemalloc vs tcmalloc vs mimalloc vs glibc

目录

打开任何一篇分配器对比文章,你会看到 micro-benchmark:malloc/free 100 万次,记录耗时。jemalloc 3.2 ns,tcmalloc 2.8 ns,mimalloc 2.5 ns,glibc 4.1 ns。

然后呢?你选了最快的那个,部署到生产环境,跑了三天,RSS 涨到了物理内存的 80%。OOM killer 来了。你切回 glibc,问题消失了。

分配器的性能不是 malloc/free 的速度。是长时间运行后的碎片率、多线程竞争下的尾延迟、以及大小类切换时的内存利用效率。

一、四个分配器的设计差异

先把架构搞清楚,然后才能理解性能数据。

glibc (ptmalloc2)

Linux 默认分配器。Arena-based 设计。

glibc 的 fastbin 在高并发下有一个反直觉的行为:它用的是单链表 + mutex,不是无锁。在高并发 malloc/free 时,arena lock 竞争是主要瓶颈。

jemalloc

FreeBSD 默认分配器,Facebook 用了十几年。

tcmalloc (Google)

Google 全家桶用的分配器。

mimalloc (Microsoft Research)

最年轻的选手,2019 年发布。

二、Micro-benchmark:参考但不要信

标准测试:单线程和多线程下 malloc/free 混合操作。对象大小分布:70% <256B, 20% 256B-4KB, 10% >4KB。

单线程

操作 glibc jemalloc tcmalloc mimalloc
malloc 64B (ns) 3.8 3.2 2.9 2.5
malloc 1KB (ns) 4.5 3.8 3.4 2.8
malloc 64KB (ns) 18 15 22 14
free (ns) 3.2 2.8 2.5 2.2
malloc+free 交替 (ns) 6.5 5.5 5.0 4.2

mimalloc 在单线程下最快。原因是 free list 在同一个 page 里,cache locality 极好。

16 线程

操作 glibc jemalloc tcmalloc mimalloc
malloc 64B (M ops/s) 38 95 120 110
malloc 1KB (M ops/s) 32 85 105 98
free (跨线程) (M ops/s) 18 52 75 68

glibc 在多线程下被吊打。arena lock 的竞争太重了。其余三个都有 per-thread cache,差距在 20% 以内。

但这些数字在真实应用中几乎没有参考价值。因为真实应用的 malloc/free 模式和 micro-benchmark 完全不同:对象生命周期不均匀、分配和释放不在同一个线程、内存使用量有波峰波谷。

三、碎片率:24 小时实测

测试方法:把四个分配器分别嵌入同一个 HTTP 服务器(基于 libevent 的 JSON API 服务),用模拟流量跑 24 小时。

流量特征: - 请求体 200B-10KB(JSON 解析 + 序列化,大量小对象) - 每个请求分配 15-30 个对象,生命周期从 100us 到 5s 不等 - 稳态 QPS: 20,000 - 每 2 小时有一次流量高峰(QPS 翻倍,持续 10 分钟)

指标 glibc jemalloc tcmalloc mimalloc
初始 RSS 48 MB 52 MB 55 MB 50 MB
4h RSS 85 MB 68 MB 92 MB 78 MB
12h RSS 145 MB 82 MB 138 MB 105 MB
24h RSS 210 MB 95 MB 185 MB 128 MB
碎片率 (RSS/实际使用) 3.5x 1.6x 3.1x 2.1x

jemalloc 在长时间运行后碎片率最低。 24 小时后 RSS 只有 95 MB,而 glibc 涨到了 210 MB。差 2.2 倍。

原因: 1. jemalloc 的 dirty page purge 机制定期把不用的页面通过 madvise(MADV_DONTNEED) 还给 OS 2. jemalloc 的 size class 更细粒度(比 glibc 多 50% 的 size class),内部碎片更小 3. glibc 的 arena 之间内存不共享,一个 arena 的空闲内存不能被另一个 arena 使用

tcmalloc 碎片率高的原因:per-thread cache 的 transfer batch 机制会预取一批对象,但如果线程的分配模式变化了(比如请求类型变了),预取的对象就浪费了。Google 内部已经意识到这个问题,新版 tcmalloc 加了更积极的回收策略。

流量高峰后的恢复

关键指标:流量高峰(QPS 翻倍 10 分钟)结束后,RSS 能不能降回去?

分配器 高峰 RSS 高峰后 30 分钟 RSS 回收率
glibc 320 MB 285 MB 11%
jemalloc 145 MB 105 MB 28%
tcmalloc 280 MB 230 MB 18%
mimalloc 195 MB 148 MB 24%

glibc 几乎不回收。这是 Linux 运维常见的困惑:“为什么我的进程 RSS 只涨不降?”——因为 glibc 的 ptmalloc 不积极做 madvise。内存 free 了但没还给 OS,留在 arena 里等下次分配。

如果你的应用有明显的波峰波谷,jemalloc 的碎片回收能力值三倍内存。

四、尾延迟:分配器也会卡你

分配器不只影响内存占用。在错误的时机,一次 malloc 可能耗时几十微秒。

测量方法:在 HTTP 请求路径上记录每次 malloc 的耗时,统计分位数。

分位数 glibc jemalloc tcmalloc mimalloc
P50 3.5 ns 3.0 ns 2.8 ns 2.4 ns
P99 45 ns 28 ns 22 ns 18 ns
P99.9 850 ns 180 ns 120 ns 95 ns
P99.99 38 us 2.5 us 8.2 us 1.8 us
最大值 210 us 15 us 42 us 12 us

glibc 的 P99.99 比其他三个高一个数量级。原因:arena lock 竞争。当多个线程同时需要从同一个 arena 分配,其中一个拿到锁,其他全部等待。38 微秒的 malloc 对于 P99.9 延迟要求在 1ms 以内的服务来说,是不可接受的。

tcmalloc 的 P99.99 也不低(8.2 us)。原因是 central free list 的锁——per-thread cache 用光后需要从 central list 批量取,这一步有锁。

mimalloc 和 jemalloc 在尾延迟上表现最好。mimalloc 的 free list sharding 设计使得几乎所有分配都能在 page-local free list 里完成,不需要任何锁。

五、选型建议

场景 推荐 理由
长时间运行的服务 (>24h) jemalloc 碎片率最低,RSS 最稳定
延迟敏感服务 (P99.9 < 1ms) mimalloc 或 jemalloc 尾延迟最低
多线程高吞吐量 tcmalloc 多线程小对象分配最快
内存受限环境 jemalloc 碎片回收最积极
通用 / 懒得换 glibc 默认就是它,没有切换成本

怎么切换:

# 编译时链接
gcc -o myapp myapp.c -ljemalloc

# 运行时注入 (不需要重编译)
LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2 ./myapp

# Rust 项目
# Cargo.toml: [dependencies] tikv-jemallocator = "0.5"

不要看完这篇文章就换分配器。先在你的应用上跑 24 小时对比测试。分配器的表现高度依赖你的 malloc/free 模式——上面的数据是这个特定 HTTP 服务器的,你的应用可能完全不同。


延伸阅读:

参考资料:

  1. Evans, J. (2006). A Scalable Concurrent malloc(3) Implementation for FreeBSD. – jemalloc 的原始论文
  2. tcmalloc 设计文档 – Google 官方
  3. Leijen, D. et al. (2019). Mimalloc: Free List Sharding in Action. Microsoft Research.
  4. glibc malloc internals – ptmalloc2 的实现细节

By .