1. 项目背景:分配路径上的一跳,值得较真吗?
先抛一个场景。几个月前我在调一个内核模块的网络收包路径,perf top 里 kmem_cache_alloc 稳稳占了前几名。顺着调用链往下看,真正的时间并不是花在伙伴系统分配页上,而是集中在 SLUB 分配器自己的内务处理里。再仔细看汇编,发现每次取下一个空闲对象时,都有一条 mov (%rax), %rbx 之类的指令,也就是从刚拿到的 object 地址上再读一次指针。这一读看似微不足道,但在每秒百万级分配的场景下,它带来的 cache miss 和内存访问开销会被放大得非常可观。
这篇想聊的,就是怎么从 slab 分配路径里干掉这次指针解引用。需要先说明一点:我们讨论的是 Linux 内核 SLUB 分配器,而不是用户态的内存池或者 Java 的 TLAB。slab 分配器的核心职责是管理同类型小对象的高效分配与释放,它通过缓存已初始化对象避免重复走伙伴系统。而这次“解引用”出现的具体位置,是在 slab_alloc_node 的 fastpath 里——从 per-CPU freelist 拿到一个 object 后,必须通过 object 内部记录的下一跳指针才能找到下一个空闲对象。这个过程,就是标题里说的 pointer dereference。
为什么值得较真?因为分配路径是内核里最热的热点之一,DDoS 防护设备、高性能网关、数据库内核模块,都重度依赖 slab 分配器。一次多余的指针读取,可能影响数千次/秒的分配吞吐;如果 object 刚刚被释放,它对应的 cache line 还可能是冷的,这一读就会触发真实的内存访问延迟。把这一跳去掉,等于把分配路径上的一块“暗礁”直接移走。这个优化适合谁看?做内核性能调优的、写驱动经常和 slab 打交道的、或者单纯对内存管理子系统感兴趣的开发者,都应该能从中获得一些思路。即使你不改 SLUB,理解这条路径上的取舍,也能帮你写出更贴近硬件心智模型的内存池代码。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 整体设计拆解:slab 分配长什么样,解引用藏在哪里
2.1 先讲清楚 slab 分配器的基本盘
要理解“去掉一次解引用”到底动了什么,得先把 SLUB 的分配主路径画出来。kmem_cache_alloc 最终会走到 slab_alloc_node,核心逻辑大致是:
c复制static __always_inline void *slab_alloc_node(struct kmem_cache *s,
gfp_t gfpflags, int node, unsigned long addr, size_t orig_size)
{
struct kmem_cache_cpu *c;
struct slab *slab;
void *object;
c = raw_cpu_read(s->cpu_slab);
object = c->freelist;
slab = c->slab;
if (unlikely(!object || !slab || !node_match(slab, node))) {
object = __slab_alloc(s, gfpflags, node, addr, c);
} else {
c->freelist = get_freepointer_safe(s, object);
stat(s, ALLOC_FASTPATH);
}
if (unlikely(slab_free_freelist_hook(s, object))) {
...
}
return object;
}
注意 c->freelist = get_freepointer_safe(s, object) 这一行。get_freepointer 的实现,本质就是从 object 的起始地址上读一个指针:
c复制static inline void *get_freepointer(struct kmem_cache *s, void *object)
{
void *fp;
fp = *(void **)(object + s->offset);
return freelist_dereference(s, fp);
}
也就是说,每次分配走 fastpath,先从 c->freelist 拿到当前空闲对象 object,然后要“读一次 object 内部的数据”才能算出下一个空闲对象是谁。在分配器看来,object 的内存是“自己人”,可以随便读;但站在整个系统层面看,这块内存在分配出去之后迟早要被用户写满数据,分配前读它,一是白白引入一次访存,二是有可能把原本不在 cache 里的 cold line 拖进 cache,提前污染。
2.2 解引用为什么会在热点路径上被放大
我最早看到这段代码的时候也想过:不就是一次指针读取吗?现代 CPU 的 store-to-load forwarding 不是吃素的,通常不会太慢。但问题在于,分配器的 fastpath 是不允许
