背景
SLUB 分配器在 per-cpu 缓存的对象耗尽时,从节点的 partial slab 链表(node partial list)上取部分空闲的 slab 来 refill。批量取(get_partial_node_bulk)时,每个选中的 slab 都从节点链表摘下、挂到本地的 pc->slabs 链表,用的是一对 remove_partial 加 list_add。这些操作全程持有节点的 n->list_lock 自旋锁。
┌────────────────────────────────────┐
│ 从节点 partial 链表批量取 slab │
└─────────────────┬──────────────────┘
▼
┌────────────────────────────────────┐
│ 逐个搬移,每个一删一加 │ 每次都改写链表指针,全程持有节点自旋锁
└─────────────────┬──────────────────┘
▼
┌────────────────────────────────────┐ 实测一次取多个连续 slab 极其频繁,逐个搬
│ 连续取多个相邻 slab 时反复抖动指针 │ 是浪费
└────────────────────────────────────┘
问题在于,取 slab 时常常一口气摘下好几个相邻 slab,它们在节点链表里本就挨着。但旧实现逐个处理:每个 slab 单独摘下、单独挂回,等于在持锁期间把同一批链表指针反复改写好多遍。这种 list 指针的 churn 纯属浪费,而且它发生在锁临界区里,直接拉长了持锁时间。
作者用 will-it-scale 的 mmap 压测统计了"一次连续取几个 slab"的分布:
| 连续 slab 数 | 出现次数 |
|---|---|
| 1 | 277,345,324 |
| 2 | 335,238,023 |
| 3 | 175,717,884 |
| ≥4 | 88,862,337 |
一次取多个连续 slab 的频次(2、3、≥4 合计约 6 亿)甚至超过只取一个的(约 2.77 亿)。也就是说,"批量取"不是偶发而是常态;逐个搬移等于把常态当特例处理。
问题
- 节点 partial 链表取 slab 时逐个摘挂
- 每个 slab 一删一加,锁内反复改写链表指针
- 一次取多个连续 slab 极其频繁,逐个搬是浪费
- 指针抖动拉长节点自旋锁的持锁时间
方案
核心是让"批量取"真正按批量搬。
get_partial_node_bulk 遍历节点链表时,追踪连续匹配的 slab run,遇到 run 就用 list_bulk_move_tail 把整段一并挪到本地链表尾,而非逐个摘挂。1 次批量搬移只改写 run 两端的指针,run 内部的指针原样不动,锁临界区里的指针 churn 大幅减少。
改动前:逐个搬移,N 次指针改写
┌──────────────────────────────────┐
│ 每个 slab 单独删除并加回本地链表 │
└────────────────┬─────────────────┘
▼
┌──────────────────────────────────┐
│ 持锁期间反复抖动链表指针 │
└──────────────────────────────────┘
改动后:连续 run 单次搬移
┌──────────────────────────┐
│ 追踪连续匹配 slab 的整段 │
└────────────┬─────────────┘
▼
┌──────────────────────────┐ 锁临界区内指针改写次数大减;重新挂回节点链表也照此
│ 用批量搬移整段挪到链表尾 │ 批量
└──────────────────────────┘
同样的批量优化也用到把剩余 partial slab 重新挂回节点链表的路径(__refill_objects_node):归还时同样按段批量搬,而非逐个 add。这 2 条路径一取一还,都从"逐个改写"改成"按段搬移"。
铺垫是把 partial slab 计数的增减、标志的置清封装成 helper,减少重复,为批量操作扫清调用点。
收益
作者用 will-it-scale 的 mmap benchmark 实测(具体硬件配置未给出):
| benchmark | 提升 |
|---|---|
| will-it-scale mmap | 2%~5% |
提升来自批量搬移减少了节点自旋锁临界区内的链表指针改写次数,缩短持锁时间;前述频率统计印证"一次取多个连续 slab"是常态,故按段搬移的收益稳定可复现。