jemalloc/Tcache 小对象路径

用线程缓存批量复用小对象

jemalloc 按 size class 为线程缓存维护指针栈;命中时直接弹出,空缓存从 arena 补充,满缓存则批量归还。

fde6a21ae内存管理核验 2026-10-12
相关概念

工程问题#

小对象频繁分配和释放时,每次都进入共享 arena 的 slab 管理会增加同步与元数据操作。缓存又不能无限留存空闲对象,否则多线程下会扩大内存占用。

实际实现#

tcache_alloc_small 选定 size class 的 cache_bin 并尝试弹栈。空时进入 tcache_alloc_small_hard,从 arena 批量填充指针数组,再弹出一个对象。普通归还在栈有容量时压入;溢出则根据保留量刷回部分对象后再压入。低水位和自适应补充、保留参数参与控制缓存大小。

处理流程#

  1. 按已确定的 size class 选择线程缓存 bin
  2. 从指针栈弹出对象,空时批量从 arena 获取
  3. 请求清零时处理对象内容,再返回对象
  4. 释放时尝试压栈;容量不足则刷回一批,再完成归还

源码片段#

固定版本 · 原文摘录
include/jemalloc/internal/tcache_inlines.hfde6a21aef364cfb762a88494b162e72e1259f5b · L103–L122
查看原文件
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);
tcache_alloc_small

按大小类选择 bin 并先尝试本地缓存;禁用的 bin 直接进入 arena。

include/jemalloc/internal/cache_bin.hfde6a21aef364cfb762a88494b162e72e1259f5b · L391–L407
查看原文件
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	}
cache_bin_alloc_impl

指针栈弹出快路径以低水位判断成功;低水位和空栈慢分支在后文。

src/tcache.cfde6a21aef364cfb762a88494b162e72e1259f5b · L430–L451
查看原文件
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);
tcache_alloc_small_hard

根据自适应填充范围向 arena 要一批指针,完成填充后再弹出一个。

include/jemalloc/internal/cache_bin.hfde6a21aef364cfb762a88494b162e72e1259f5b · L481–L499
查看原文件
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}
cache_bin_dalloc_easy

检查容量与安全条件后将对象指针压入缓存栈。

include/jemalloc/internal/tcache_inlines.hfde6a21aef364cfb762a88494b162e72e1259f5b · L209–L224
查看原文件
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	}
tcache_dalloc_small

满缓存依据保留参数刷回一批对象,再重试压栈;不是固定一半规则。

摘录分别保留调用入口、核心机制及边界;完整上下文见各段源码链接。

收益与取舍#

工程分析

带来的收益

  • 缓存命中时只需本地指针操作,不必每次进入共享 slab 分配路径
  • 批量补充与归还把 arena 管理成本分摊到多次对象操作

付出的代价

  • 每线程、每大小类都可能保留空闲对象,线程数增多会放大缓存占用
  • 补充、刷回、清零和安全检查不是常数成本;不能把栈操作成本等同完整 malloc/free 延迟
  • 本例只针对启用且适用的小对象 tcache,禁用、特殊对齐或其他大小路径会绕过或进入额外处理

可比较的选择

每次直接访问 arena减少线程缓存留存,但更频繁地进入共享元数据管理。

固定批量、固定保留量缓存策略直接,面对不同分配释放模式时较难兼顾占用与补充频率。

版本与适用范围#

固定到提交 fde6a21aef36。固定提交包含自适应补充/保留及 stash 路径,不能套用旧版固定半满刷回描述。片段展示小对象 tcache 入口与存储机制,不覆盖所有分配入口、跨线程所有权及完整 arena 锁流程。

参考来源#

源码、官方文档与原始论文
01
jemalloc · tcache_alloc_small

include/jemalloc/internal/tcache_inlines.h · tcache_alloc_small

源码fde6a21aef364cfb762a88494b162e72e1259f5b
02
jemalloc · cache_bin_alloc_impl

include/jemalloc/internal/cache_bin.h · cache_bin_alloc_impl

源码fde6a21aef364cfb762a88494b162e72e1259f5b
03
jemalloc · tcache_alloc_small_hard

src/tcache.c · tcache_alloc_small_hard

源码fde6a21aef364cfb762a88494b162e72e1259f5b
04官方文档官方文档;实现以固定提交为准

相关实现与比较#

4 个相关案例
nginx·HTTP upstream 配置解析release-1.26.3

nginx 解析 upstream 块时,用 ngx_array_t 连续保存服务器条目;每遇到一条 server 指令便追加一个元素。

附源码片段 · 通信与流量
19 个匹配案例