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

原文发布于 quant67.com,转载请保留出处。

打开任何一篇分配器对比文章,你会看到 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 设计。

  • 每个线程绑定一个 arena(最多 8 × CPU 核数 个 arena)
  • 小对象(<512B)用 fastbins + smallbins,LIFO 链表
  • 大对象(>128KB)直接 mmap
  • 碎片来源:bins 之间的间隙 + arena 之间的不均匀

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

jemalloc

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

  • Thread cache + arena + extent 三层架构
  • 小对象按 size class 分桶(8B, 16B, 32B, 48B, ...),每个桶预分配 slab
  • 碎片控制:定期做 dirty page purge,把不用的页面还给 OS
  • 优势:碎片率控制最好,长时间运行 RSS 最稳定

tcmalloc (Google)

Google 全家桶用的分配器。

  • Per-thread cache + central free list + page heap 三层
  • Transfer batch:线程 cache 用光时,从 central list 批量拿一批(减少锁竞争)
  • 优势:多线程小对象分配速度最快
  • 弱点:碎片控制不如 jemalloc,大对象分配较慢

mimalloc (Microsoft Research)

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

  • Per-thread heap,每个 heap 由固定大小的 page 组成
  • Free list sharding:把 free list 分散到每个 page 里,减少 cache miss
  • 延迟释放:其他线程释放的块先放进 local free list,稍后再合并
  • 优势:micro-benchmark 最快,cache 友好度最高

二、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 默认就是它,没有切换成本

怎么切换:

bash 复制代码
# 编译时链接
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 的实现细节
相关推荐
ltl44 分钟前
Zero Copy 的肮脏秘密:在什么时候反而更慢
linux
数据知道1 小时前
宽字节注入、二次注入、堆叠注入:高级 SQLi 专题
linux·运维·安全·网络安全
童槿顏丶2 小时前
GitLab部署到Linux
linux·运维·gitlab
空堂与归5 小时前
Linux 命令实战:AI 开发者必备的终端操作手册
linux
牢姐与蒯7 小时前
Linux基本指令及知识点(二)
linux·运维·服务器
小玮看世界7 小时前
[Python]从“脏”数据到优雅实现:一个IoT滑动窗口最大值问题的测试驱动优化实录
linux·前端·python
RisunJan8 小时前
Linux命令-spell(Unix 拼写检查工具 —— 文本校对的基础利器)
linux·运维·unix
BLi4ee8 小时前
WSL 文件资源管理器左侧 Linux / Ubuntu 入口恢复
linux·ubuntu
BLUcoding9 小时前
Ubuntu 开机自启 Kiosk 模式浏览器全屏展示网页配置指南
linux·运维·ubuntu