jemalloc 的参数和优化思路

一、参数

1
2
3
export MALLOC_CONF="narenas:8,dirty_decay_ms:5000,muzzy_decay_ms:5000"
export MALLOC_CONF="narenas:16,dirty_decay_ms:5000,muzzy_decay_ms:5000,junk:false"

  • backgroud_thread:启用/禁用内部后台工作线程。当设置为 true 时,将根据需要创建后台线程(后台线程的数量不会超过 CPU 或活跃 arena 的数量)。线程定期运行,并且异步进行清理操作

  • max_background_threads:将创建的后台工作线程的最大数量

  • opt.metadata_thp:是否允许 jemalloc 对内部元数据使用透明大页。“always” 表示允许。“auto” 表示最初不使用透明大页,但当元数据使用达到一定水平时可能会开始这样做。默认为 “disabled” 禁用。

  • opt.retain:如果为 true,则保留未使用的虚拟内存以供以后重用,而不是通过 munmap 来释放。他还使 jemalloc 以更加贪婪的方式使用 mmap,一次性申请更大的内存块。默认情况下此选项被禁用。

  • opt.narenas:最大的 arena 数量,默认是 CPU 个数的四倍,如果只有一个 CPU,则默认为一倍

  • opt.oversize_threshold:被视为大内存块的阈值。比此阈值更大的内存块将通过专门的 arena 来管理。这样可以不让大内存块和小内存块混在一起分配,从而减少内存碎片。默认阈值为 8M

  • opt.dirty_decay_msopt.muzzy_decay_ms

    1
    2
    3
    4
    5
    6
    7
    8
    9
    尽管我们希望将所有的 malloc、free 内存都可以放在 tcache 中或者 bin 中,这样可以最大化执行效率,但是实际的程序中这很难做到,因为每个线程都需要增加内存,会造成不小的内存压力,而且内存的申请释放往往会有波峰,dirty extents 和 muzzy extents 就可以来应对这些内存申请的波峰,而避免需要转入内核态来重新申请内存页。

    dirty_decay_ms 和 muzzy_decay_ms 是 jemalloc 中用来控制长时间空闲内存衰变的时间参数,适当地扩大 dirty decay 的时间可以有效地解决性能劣化的尖刺。

    脏页定义是,以前可能被应用程序写入,因此消耗物理内存,但当前没有使用的内存。
    dirty_decay_ms 为 0 会导致所有未使用的脏页在创建后立即被清除。设置为 -1 表示禁用此功能。默认是 10 秒

    模糊页面定义是,以前未使用过的脏页,后面可能被操作系统的某些事件触发,而被清理的内存,这种内存处于不确定状态。
    muzzy_decay_ms 为 0 会立即清除,-1 表示禁用清理。默认为 10 秒。
  • opt.junk:内存空间填充。如果设置为 “alloc”,则分配内存时会初始化为 0xa5。如果设置为 “free”,则释放内存时会初始化为 0x5a。如果设置为 “true”,则分配和释放的内存都会被初始化。如果设置为 “false”,则此功能被禁用。默认为 false。此功能会对性能有影响。

  • opt.zero:如果启用,分配内存时初始化为 0。注意,此初始化对每个字节仅发生一次,因此对于 realloc 之类的调用不会将先前分配的内存归零。默认为禁用。此功能对性能有影响。

  • opt.tcache:启用/禁用线程的私有缓存。使用线程局部缓存不会产生锁竞争,但是代价是内存占用增加了。默认启用此选项

二、优化思路

jemalloc 对于内存用的是多级缓存的思路,tcache 的代价最小,无须加锁可以直接返回;其次是 arena 的 bin->extent,锁的粒度在对应的 bin 上,会使 bin 对应的 size 在这个 arena 中无法再做 fill 或 flush;然后是 dirty extent、muzzy extent,这部分是 arena 全局加锁,会锁住其他线程的 fill 或者 purge。那么在多线程下,我们可以用几个思路来优化锁的竞争。

1. arena 优化

每个 arena 管理多个线程,arena 的数量默认是 CPU 个数的 4 倍。jemalloc 将锁的范围控制在 arena 级别。如果我们的线程数特别多,那我们可以增加 arena 的个数。让每个 arena 管理的线程数减少,线程之间产生的锁竞争的概率就会减少。

此外,还可以使用 mallocx 隔离线程,让内存分配任务比较重的线程独占 arena。

2. slab size 优化

slab size 的大小一般为 usize 大小和 pagesize 的最小公倍数,这一机制可以保证减少内存碎片,但是 tcache 的 fill 和 flush 都与 slab size 大小相关,如果 slab size 和业务的内存模型相匹配才可以得到更好的性能效果。可以根据具体场景适当调大 slab size 来减少开销。

待补充

3. dirty decay 和 muzzy decay

尽管我们希望将所有的 malloc、free 内存都可以放在 tcache 中或者 bin 中,这样可以最大化执行效率,但是实际的程序中这很难做到,因为每个线程都需要增加内存,会造成不小的内存压力,而且内存的申请释放往往会有波峰,dirty extents 和 muzzy extents 就可以来应对这些内存申请的波峰,而避免需要转入内核态来重新申请内存页。

dirty_decay_ms 和 muzzy_decay_ms 是 jemalloc 中用来控制长时间空闲内存衰变的时间参数,适当地扩大 dirty decay 的时间可以有效地解决性能劣化的尖刺。

4. tcache ncached_max

tcache 中每一个 bin 的 slots 数量由 ncached_max 决定,当 tcache 中 ncached_max 耗尽时会触发 arena 的 fill tcache 而产生锁,而 ncached_max 的大小默认为 2 * slab size,最小为 20,最大为 200,适当地扩大 ncached_max 值可以在一些线程上形成更优的 allocation/deallocation 循环

三、优化实践方法

我们可以通过 jemalloc 提供的接口:malloc_stats_print 函数,dump 出程序当前的内存分配信息:

1
2
3
4
// reference: https://jemalloc.net/jemalloc.3.html
void malloc_stats_print(void (*write_cb)(void *, const char *), // 回调函数,可以写入文件
void *cbopaque, // 回调函数参数
const char *opts); // stats的一些选项,如"J"是导出json格式

或者设置 malloc_conf,在程序运行结束后自动 dump stats:

1
export MALLOC_CONF=stats_print:true

然后对于生成的结果文件。我们去查看如下信息:

1. arena 数量与线程比例

arena 数量:jemalloc->arenas->narenas

线程数量:jemalloc->stats.arenas->merged->nthreads