背景
SLUB 的 sheaves 是 per-CPU 的对象批量缓存。sheaf 是一个固定容量的对象指针数组,调用方(如 maple tree 的 maple_node)用 prefill API 批量预取对象、用完批量归还,绕开逐对象分配的慢路径。barn 是每个 node 的 sheaf 仓库,分 full 与 empty 两个列表,供 CPU 之间互通有无。kfree_rcu() 释放的对象先聚成 RCU sheaf,之后尝试放进 barn 的 full 列表,放不进去就把对象逐个 flush 回 slab。
现有 sheaf 机制的对象流转
┌─────────────────────────────────────┐
│ prefill API 给未满的 sheaf 补对象, │
│ 只从 partial slab 取 │
└──────────────────┬──────────────────┘
▼
┌─────────────────────────────────────┐
│ 调用方从 sheaf 批量取用对象 │
└──────────────────┬──────────────────┘
▼
┌─────────────────────────────────────┐
│ kfree_rcu 的对象聚成 RCU sheaf 交还 │
│ barn │ barn 的 full 列表只进不出,逐渐饱和
└─────────────────────────────────────┘
现有的补充逻辑有个偏向,prefill API 给未满的 sheaf 补对象时只从 partial slab 取,从来不从 barn 的 full 列表取。
问题
- sheaf 未满时只从 partial slab 补充,从不消费 barn 的 full 列表。
- barn 的 full 列表一旦饱和就一直饱和,只进不出。
- kfree_rcu 的 RCU sheaf 无处安放,只能 flush 回 slab。
- 从 partial slab 补充要走 list_lock,flush 让对象回流慢路径。
方案
让补充优先从 barn 取,barn 上新增一个 partial sheaf,专门收留补剩下的对象,不再 flush。
做法是把 barn 的对象拷进调用方手里的 sheaf,而不是拿 barn 的 sheaf 换给它。prefill 路径先调新增的 refill_sheaf_from_barn():在 barn 锁内循环拷贝,直到 sheaf 填满或 barn 掏空。每轮先取 barn 的 partial sheaf 作拷贝源,没有就从 full 列表取一个;full 列表上的 sheaf 未必是满的,所以每轮按缺口与源存量的较小值拷贝。源拷贝后还有剩就留作 partial sheaf,掏空了就放 empty 列表,被补充的 sheaf 始终不换出。barn 掏不出对象时返回 false,回退原有的 partial slab 路径。
从 barn 原地补充未满的 sheaf
┌──────────────────────────────────────┐
│ 循环拷贝直到 sheaf 填满或 barn 掏空 │ 调用方的 sheaf 始终不换出
└──────────────────┬───────────────────┘
▼
┌──────────────────────────────────────┐
│ 拷贝源先取 barn 的 partial sheaf,没 │
│ 有则从 full 列表取一个 │
└──────────────────┬───────────────────┘
▼
┌──────────────────────────────────────┐
│ 源拷贝后还有剩就留作 partial sheaf, │
│ 掏空就放 empty 列表 │
└──────────────────────────────────────┘
只有 prefill 路径需要这个 partial sheaf。generic 分配路径 __pcs_replace_empty_main() 是拿空 sheaf 换 barn 的 full sheaf,不会有剩余;refill_sheaf() 只从 partial slab 取,不碰 partial sheaf。
也可以直接用 barn 的 sheaf 换掉调用方的 sheaf,省掉这次 memcpy。但换掉 sheaf 会丢失 cache 亲和性,原地填充更直白,实测两者没有可测差异,于是采用原地填充;交换要附加的 pfmemalloc 约束与容量修正也随之不再需要。
partial sheaf 是点睛之笔。把未满的 sheaf 直接挂上 full 列表行不通,它占着位置,full 列表依旧饱和,RCU sheaf 照样无处安放。有了 partial sheaf,剩余对象既不丢回 slab,也不占 full 列表名额。
收益来自两侧。每取走一个 full sheaf,就给未来的 RCU sheaf 腾出一个位置,barn 的循环被打通。从 barn 拿对象也比在 list_lock 下从 partial slab 拿便宜。
barn 的循环被打通
┌────────────────────────┐ ┌───────────────────┐ ┌────────────┐
│ │ │ │ │ RCU sheaf │
│ refill 取走 full sheaf ├──▶│ full 列表腾出位置 ├──▶│ 进 barn 免 │
│ │ │ │ │ 于 flush │
└────────────────────────┘ └───────────────────┘ └────────────┘
收益
will-it-scale mmap1 基准(192 进程,每次一分钟),maple_node cache:
| 指标 | baseline | patched | 变化 |
|---|---|---|---|
| 吞吐 | 27778727 | 34500556 | +24.2% |
| alloc_slab | 7100651 | 159739 | -97.75% |
| free_slab | 7099788 | 158297 | -97.77% |
| page allocator churn | 14200439 | 318036 | -97.76% |
| free_slowpath | 10969156 | 782851 | -92.86% |
| sheaf_refill | 8343794364 | 4657509724 | -44.18% |
| sheaf_flush | 8332823922 | 4656725764 | -44.12% |
| barn_put_fail | 260400484 | 145522675 | -44.12% |
代价方面,测试后 maple_node cache 持有的 slab 从 1604 增至 2336,作者手动 shrink 后为 485 对 563,说明多出的 slab 大多只是对象坐在 sheaf 里,shrink 即可归还;剩余的少许差异,是因为从 barn 发出去的对象来自的 slab 比从 partial slab 补充更分散。