# 第 10 章实验:缓存亲和性(cache affinity) 对应 OSTEP 第 10 章《多处理器调度》的 10.3 节:一个进程在某个 CPU 上运行一段时间后, 会在那个 CPU 的缓存(和 TLB)里积累大量状态;下次还在同一个 CPU 上运行就快, 换到别的 CPU 上就得重新把数据搬过来。本实验直接测量这个"重新预热"的代价。 ## 怎么跑 ```bash cd labs/10-multicpu make ./affinity # 扫描 16KB ~ 64MB 多种工作集大小,约 5 秒 ./affinity 256 # 只测 256KB 工作集 ./affinity 256 401 0 1 # 指定在 CPU 0 和 CPU 1 之间迁移(可以试试两个超线程兄弟) ``` 编译参数:`gcc -O2 -Wall`。 ## 程序做了什么 - 把工作集切成 64 字节的缓存行,串成一个**随机顺序**的环(每行存着下一行的地址)。 "走一遍"就是沿环访问每一行一次,并写一下这一行(让它在缓存里变脏)。 随机顺序 + 指针追逐让硬件预取器猜不到下一个地址,测到的就是真实的访存延迟。 - **pin**:用 `sched_setaffinity()` 把自己固定在 CPU 0,反复走。 - **migrate**:每走完一遍,就把自己换到另一个 CPU(默认 CPU 0 ↔ CPU 2)再走。换 CPU 的系统调用开销不计时。 - 每种配置取各遍耗时的中位数,折算成"每条缓存行多少纳秒"。 ## 该观察到什么 示例输出(AMD Ryzen 7 8745H:每核 32KB L1d、1MB L2,8 核共享 16MB L3;WSL2。你的机器上数字会不同): ``` 工作集 pin migrate migrate/pin 16 KB 1.49 11.74 7.9x 64 KB 2.91 15.13 5.2x 256 KB 2.87 16.59 5.8x 1 MB 7.66 13.99 1.8x 4 MB 11.47 16.83 1.5x 16 MB 94.21 95.82 1.0x 64 MB 126.10 118.63 0.9x ``` - **工作集装得进本核私有缓存(L1/L2)时,迁移慢 4~8 倍。** 固定在一个 CPU 上,每次访问都命中 L1/L2(1~3 ns); 一换 CPU,新 CPU 的 L1/L2 里什么都没有,而最新的(脏的)数据还在旧 CPU 的缓存里, 每一行都要通过缓存一致性协议从旧 CPU 那边拿过来(十几 ns)。这就是书里说的"缓存亲和性"。 - **工作集只装得进共享的 L3 时,差距缩小到接近 1 倍。** 两个 CPU 都得去 L3 取,谁在哪儿跑都差不多。 - **工作集超过 L3 时,没有差别。** 无论在哪都要去内存(~100 ns),亲和性就无关紧要了。 (这里每条行 ~100 ns 还包括随机访问造成的 TLB 未命中,见第 19 章。) ## 和书里的联系 - 调度器(无论单队列 SQMS 还是多队列 MQMS)都应尽量让进程留在同一个 CPU 上,除非为了负载均衡不得不迁移。 - 迁移的代价是**一次性**的:换过去以后,新 CPU 的缓存很快就热了。所以偶尔迁移(工作窃取、周期性均衡)是可以接受的, 频繁地来回迁移(像本实验的 migrate 那样每遍都换)才会一直付出这个代价。 - 在虚拟机(包括 WSL2)里,"CPU"是虚拟 CPU,宿主机可能把它们调度到任意物理核上, 所以 `/sys/.../thread_siblings_list` 报告的超线程兄弟不一定真的共享 L1/L2,结果会更嘈杂。