背景
struct mem_cgroup 里有两类访问频率都很高的成员:vmstats_percpu 指针,memcg 统计路径每次读写计数都要先加载它;memory_events_local[] 计数器,memory.events 的本地事件计数,swap 失败等事件直接写它。此前的 cgroup v2 瘦身系列把 v1 用户态 eventfd 处理移入 memcontrol-v1.c,CONFIG_MEMCG_V1 关闭时 struct vmpressure 从 112 字节缩到 24 字节,结构体后续成员随之上移,memory_events_local[MEMCG_SWAP_FAIL] 与 vmstats_percpu 落到了同一 cacheline。
结构瘦身引发的共行
┌──────────────────────────────────────┐
│ vmpressure 从 112 缩到 24 字节 │
└──────────────────┬───────────────────┘
▼
┌──────────────────────────────────────┐
│ MEMCG_SWAP_FAIL 与 vmstats_percpu 落 │
│ 到同一 cacheline │
└──────────────────┬───────────────────┘
▼
┌──────────────────────────────────────┐
│ 写事件计数使该行失效,统计路径反复加 │
│ 载该指针 │ false sharing
└──────────────────────────────────────┘
问题
- 事件计数写入与 vmstats_percpu 加载共 cacheline,每次写入都让统计路径的缓存失效。
- stress-ng mremap 在 swap 关闭时走 MADV_PAGEOUT,每 60 秒约 2000 万次 MEMCG_SWAP_FAIL 更新。
- 176 CPU 测试机上 mremap 吞吐中位数下降 4.38%。
方案
把两者隔开,让 vmstats_percpu 独占 cacheline。
cgwb_list 上移填入 vmpressure 缩小后留下的对齐空隙,vmstats_percpu 标记 ____cacheline_aligned_in_smp 对齐到新的缓存行,事件计数器与热点指针从此分行。在测试配置下 struct mem_cgroup 总大小不变,空间来自既有空隙而非新增成员。
指针与计数器分行
┌───────────────────────────────────┐
│ cgwb_list 移入对齐空隙 │
└─────────────────┬─────────────────┘
▼
┌───────────────────────────────────┐
│ vmstats_percpu 缓存行对齐独占一行 │
└─────────────────┬─────────────────┘
▼
┌───────────────────────────────────┐
│ 事件写与统计读不再共享 cacheline │
└───────────────────────────────────┘
收益
stress-ng mremap stressor,176 CPU 单 socket 测试机,以被责怪提交的父提交为基准:
| 指标 | Before | After |
|---|---|---|
| mremap 吞吐中位数相对父提交 | -4.38% | 0.5% 以内 |
观测到的 boot-to-boot 波动最高 1.2%,补丁后的差距落在波动范围内,即吞吐已恢复。