背景
SLUB 在 per-CPU 缓存耗尽、需要一块新 slab 时,会从新 slab 分配对象来 refill。问题出在新 slab 的初始化上:按旧实现,slab 一分配出来就先在里头构建好完整的 freelist,然后才开始往外发对象。
┌──────────────────────────┐
│ 从新 slab 分配对象 │
└────────────┬─────────────┘
▼
┌──────────────────────────┐
│ 分配时就构建完整空闲链表 │ 若对象被全部分配走,空闲链表立即丢弃
└────────────┬─────────────┘
▼
┌──────────────────────────┐
│ 这部分构建工作纯属浪费 │
└──────────────────────────┘
可一次 fresh-slab 分配往往把这块 slab 的对象全部消耗掉(批量分配尤其如此),那意味着分配期间辛苦构建的完整 freelist 转眼就被丢弃。这部分构建工作没有服务到任何后续分配,纯属浪费。
问题
- 新 slab 分配期就构建完整 freelist
- 对象被全部分配走则 freelist 立即丢弃
- 这部分构建工作没服务任何分配
- 批量分配尤其常见这种浪费
方案
核心是把 freelist 构建推迟到对象 emit 之后,只为真正剩下的对象构建。
旧:分配 slab 即构建完整空闲链表
┌────────────────────────────────┐
│ 新 slab 分配期间建完整空闲链表 │
└───────────────┬────────────────┘
▼
┌────────────────────────────────┐
│ 对象全发走则空闲链表白建 │
└────────────────────────────────┘
新:先发对象,只为剩余建空闲链表
┌────────────────────────────────┐
│ 新 slab 只初始化,不建空闲链表 │
└───────────────┬────────────────┘
▼
┌────────────────────────────────┐
│ 发完对象再为剩余建空闲链表 │ 一条迭代器抽象同时填 sheaf 与建剩余空闲链表
└────────────────────────────────┘
具体地,new_slab 只负责分配 slab 和初始化 metadata,不再构建 freelist。refill_objects 拿到新 slab 后由 alloc_from_new_slab 直接 emit 对象,等对象发完,只为剩余未分配的对象构建 freelist;单对象的 alloc_single_from_new_slab 同样改。为了 RANDOM=y/n 走同一条路,引入一个小的 iterator 抽象,按分配顺序遍历 free objects,它同时用于填 sheaf 和构建剩余对象的 freelist。
顺带把 setup_object 显式标 inline:这次优化后编译器不再稳定内联这个热路径 helper,可能反而损害性能,显式 inline 把代码生成恢复到预期。
因为这套改动落在 refill_objects 这条 fresh-slab refill 路径上,而 refill_objects 也用于 refill sheaves,受益的不只是 kmem_cache_alloc_bulk 的用户,普通分配在走到 fresh-slab refill 时同样少花这份冤枉钱。
收益
作者用 slub_bulk_bench 实测(qemu-system-x86,1024M,smp 8,-enable-kvm -cpu host;Kernel 7.1.0-rc1-next-20260429;x86_64_defconfig;Rounds 20;Total 256MB),per-object 分配时间:
CONFIG_SLAB_FREELIST_RANDOM=n:
| obj_size | batch | Before (ns/object) | After (ns/object) | 变化 |
|---|---|---|---|---|
| 16 | 256 | 5.44 | 3.12 | -42.6% |
| 32 | 128 | 7.57 | 3.79 | -49.9% |
| 64 | 64 | 11.27 | 4.83 | -57.2% |
| 128 | 32 | 19.38 | 6.43 | -66.8% |
| 256 | 32 | 23.59 | 6.97 | -70.5% |
| 512 | 32 | 21.06 | 7.12 | -66.2% |
CONFIG_SLAB_FREELIST_RANDOM=y:
| obj_size | batch | Before (ns/object) | After (ns/object) | 变化 |
|---|---|---|---|---|
| 16 | 256 | 9.42 | 4.36 | -53.7% |
| 32 | 128 | 12.19 | 4.93 | -59.6% |
| 64 | 64 | 17.01 | 6.14 | -63.9% |
| 128 | 32 | 23.71 | 8.35 | -64.8% |
| 256 | 32 | 29.20 | 9.44 | -67.7% |
| 512 | 32 | 29.35 | 9.21 | -68.6% |
per-object 开销降约 42%~70%(RANDOM=n)、58%~69%(RANDOM=y),对象越大降幅越显著。需说明:slub_bulk_bench 旨在隔离本变更移除的开销,每次迭代正好从新 slab 分配 slab->objects 个对象,是 deferred freelist 的近最佳场景;实际工作负载的端到端增益会小一些,取决于多常到达 fresh-slab refill 路径。