用线程缓存批量复用小对象
jemalloc 按 size class 为线程缓存维护指针栈;命中时直接弹出,空缓存从 arena 补充,满缓存则批量归还。
工程问题#
小对象频繁分配和释放时,每次都进入共享 arena 的 slab 管理会增加同步与元数据操作。缓存又不能无限留存空闲对象,否则多线程下会扩大内存占用。
实际实现#
tcache_alloc_small 选定 size class 的 cache_bin 并尝试弹栈。空时进入 tcache_alloc_small_hard,从 arena 批量填充指针数组,再弹出一个对象。普通归还在栈有容量时压入;溢出则根据保留量刷回部分对象后再压入。低水位和自适应补充、保留参数参与控制缓存大小。
处理流程#
- 按已确定的 size class 选择线程缓存 bin
- 从指针栈弹出对象,空时批量从 arena 获取
- 请求清零时处理对象内容,再返回对象
- 释放时尝试压栈;容量不足则刷回一批,再完成归还
源码片段#
固定版本 · 原文摘录103tcache_alloc_small(tsd_t *tsd, arena_t *arena, tcache_t *tcache, size_t size,104 szind_t binind, bool zero, bool slow_path) {105 void *ret;106 bool tcache_success;107108 assert(binind < SC_NBINS);109 cache_bin_t *bin = &tcache->bins[binind];110 ret = cache_bin_alloc(bin, &tcache_success);111 assert(tcache_success == (ret != NULL));112 if (unlikely(!tcache_success)) {113 bool tcache_hard_success;114 arena = arena_choose(tsd, arena);115 if (unlikely(arena == NULL)) {116 return NULL;117 }118 if (unlikely(tcache_bin_disabled(119 binind, bin, tcache->tcache_slow))) {120 /* stats and zero are handled directly by the arena. */121 return arena_malloc_hard(tsd_tsdn(tsd), arena, size,122 binind, zero, /* slab */ true);按大小类选择 bin 并先尝试本地缓存;禁用的 bin 直接进入 arena。
391 /*392 * This may read from the empty position; however the loaded value won't393 * be used. It's safe because the stack has one more slot reserved.394 */395 void *ret = *bin->stack_head;396 cache_bin_sz_t low_bits = (cache_bin_sz_t)(uintptr_t)bin->stack_head;397 void **new_head = bin->stack_head + 1;398399 /*400 * Note that the low water mark is at most empty; if we pass this check,401 * we know we're non-empty.402 */403 if (likely(low_bits != bin->low_bits_low_water)) {404 bin->stack_head = new_head;405 *success = true;406 return ret;407 }指针栈弹出快路径以低水位判断成功;低水位和空栈慢分支在后文。
430 cache_bin_sz_t ncached_max = cache_bin_ncached_max_get(cache_bin);431 cache_bin_sz_t nfill = tcache_slow->bin_nfill[binind];432 assert(nfill > 0 && nfill <= ncached_max);433 /* Let the arena finish draining a slab beyond the adaptive minimum. */434 cache_bin_sz_t nfill_min = nfill;435 cache_bin_sz_t nfill_max = tcache_ncached_target_max(ncached_max);436 CACHE_BIN_PTR_ARRAY_DECLARE(ptrs, nfill_max);437 cache_bin_init_ptr_array_for_fill(cache_bin, &ptrs, nfill_max);438439 cache_bin_sz_t filled = arena_ptr_array_fill_small(tsdn, arena, binind,440 &ptrs, /* nfill_min */ nfill_min, /* nfill_max */ nfill_max,441 cache_bin->tstats);442 cache_bin_finish_fill(cache_bin, &ptrs, filled);443 assert(filled >= nfill_min && filled <= nfill_max);444 assert(cache_bin_ncached_get_local(cache_bin) == filled);445446 tcache_slow->bin_nretain[binind] =447 tcache_ncached_retain_after_refill(448 tcache_slow->bin_nretain[binind], filled, ncached_max);449 tcache_slow->bin_nfill[binind] = tcache_ncached_fill_after_refill(450 tcache_slow->bin_nfill[binind], ncached_max);451 ret = cache_bin_alloc(cache_bin, tcache_success);根据自适应填充范围向 arena 要一批指针,完成填充后再弹出一个。
481 * Free an object into the given bin. Fails only if the bin is full.482 */483JEMALLOC_ALWAYS_INLINE bool484cache_bin_dalloc_easy(cache_bin_t *bin, void *ptr) {485 if (unlikely(cache_bin_full(bin))) {486 return false;487 }488489 if (unlikely(cache_bin_dalloc_safety_checks(bin, ptr))) {490 return true;491 }492493 bin->stack_head--;494 *bin->stack_head = ptr;495 cache_bin_assert_earlier(bin, bin->low_bits_full,496 (cache_bin_sz_t)(uintptr_t)bin->stack_head);497498 return true;499}检查容量与安全条件后将对象指针压入缓存栈。
209 if (unlikely(!cache_bin_dalloc_easy(bin, ptr))) {210 if (unlikely(tcache_bin_disabled(211 binind, bin, tcache->tcache_slow))) {212 arena_dalloc_small(tsd_tsdn(tsd), ptr);213 return;214 }215 cache_bin_sz_t max = cache_bin_ncached_max_get(bin);216 cache_bin_sz_t *nretain =217 &tcache->tcache_slow->bin_nretain[binind];218 *nretain = tcache_ncached_retain_after_overflow(*nretain, max);219 unsigned remain =220 (unsigned)tcache_ncached_flush_remain(*nretain, max);221 tcache_bin_flush_small(tsd, tcache, bin, binind, remain);222 bool ret = cache_bin_dalloc_easy(bin, ptr);223 assert(ret);224 }满缓存依据保留参数刷回一批对象,再重试压栈;不是固定一半规则。
摘录分别保留调用入口、核心机制及边界;完整上下文见各段源码链接。
收益与取舍#
工程分析带来的收益
- 缓存命中时只需本地指针操作,不必每次进入共享 slab 分配路径
- 批量补充与归还把 arena 管理成本分摊到多次对象操作
付出的代价
- 每线程、每大小类都可能保留空闲对象,线程数增多会放大缓存占用
- 补充、刷回、清零和安全检查不是常数成本;不能把栈操作成本等同完整 malloc/free 延迟
- 本例只针对启用且适用的小对象 tcache,禁用、特殊对齐或其他大小路径会绕过或进入额外处理
可比较的选择
每次直接访问 arena减少线程缓存留存,但更频繁地进入共享元数据管理。
固定批量、固定保留量缓存策略直接,面对不同分配释放模式时较难兼顾占用与补充频率。
版本与适用范围#
固定到提交 fde6a21aef36。固定提交包含自适应补充/保留及 stash 路径,不能套用旧版固定半满刷回描述。片段展示小对象 tcache 入口与存储机制,不覆盖所有分配入口、跨线程所有权及完整 arena 锁流程。
参考来源#
源码、官方文档与原始论文include/jemalloc/internal/tcache_inlines.h · tcache_alloc_small
include/jemalloc/internal/cache_bin.h · cache_bin_alloc_impl
src/tcache.c · tcache_alloc_small_hard
相关实现与比较#
4 个相关案例nginx 解析 upstream 块时,用 ngx_array_t 连续保存服务器条目;每遇到一条 server 指令便追加一个元素。
Linux IIO 的 kfifo 缓冲在采样进入内核与用户态读取之间暂存数据,按先进先出顺序交付采样记录。
Linux BPF 验证器的 check_cfg 使用显式指令栈执行非递归 DFS,检查跳转目标、可达性及控制流结构。
Git 把内存中的 index 条目指针保存在有序、可增长的数组中,按路径和合并 stage 二分查找位置。