# 第 19 章实验:测量 TLB 的大小和未命中代价 对应原书第 19 章的 Homework (Measurement)。思路来自 Saavedra-Barrera:在一个大数组里**每页只访问一个 int**, 访问 N 个页并重复很多趟,算出每次访问的平均耗时;N 超过 TLB 的项数时,耗时会突然跳高。 ## 文件 | 文件 | 作用 | |---|---| | `timer.c` | 作业第 1 题:`clock_gettime` / `gettimeofday` 的精度有多高 | | `tlb.c` | 测量程序:`./tlb <页数> <趟数> [选项]` | | `run.sh` | 作业第 3 题:页数从 1 扫到 24576(×2,中间插 ×1.5 的点) | ## 怎么跑 ```bash make ./timer ./tlb 64 100000 # 访问 64 页,重复 10 万趟 ./tlb 4096 2000 -p # 指针追逐模式 ./run.sh # 原书的 a[i] += 1 循环 ./run.sh -p # 指针追逐(测延迟,拐点更清楚) ./run.sh -s # 按顺序访问页(默认是随机顺序) ``` `tlb` 的选项:`-c CPU` 绑定到哪个 CPU(默认 0,`-1` 不绑定);`-s` 顺序访问;`-n` 不错开缓存行; `-k K` 每个点重复测 K 次(默认 5),报告最小值和中位数;`-H` 允许透明大页;`-p` 指针追逐;`-q` 只输出一行。 ## 作业题的对应处理 1. **计时器精度**(第 1 题):`timer` 显示 `clock_gettime` 分辨率 1 ns,但连续两次调用本身就要约 10~20 ns; `gettimeofday` 只能分辨 1 µs。一次页访问只有几纳秒,所以必须把访问重复上百万次,让总时间达到毫秒级。 `run.sh` 让每个点总共访问约 1600 万次(环境变量 `TOTAL` 可改)。 2. **防止编译器优化**(第 5 题):数组指针声明为 `volatile int *`,每次 `a[i] += 1` 都必须真的读写内存; 程序最后还会把数组求和并"使用"结果。用 `-O2` 编译,`objdump -d tlb` 可以确认循环还在。 3. **绑定 CPU**(第 6 题):`sched_setaffinity` 把线程固定在一个 CPU 上(等价于 `taskset -c 0 ./tlb ...`)。 每个核有自己的 TLB,被迁移到另一个核会让 TLB 变冷、结果出现尖峰。 4. **初始化**(第 7 题):计时前先把每一页写一遍,把缺页和按需清零(demand zeroing)的开销挪到计时之外; 再热身 0.2 秒,让 TLB、缓存和 CPU 频率都进入稳定状态。 5. **额外的两个陷阱**(原书没提,但在现代机器上很重要): - **透明大页(THP)**:如果内核用 2MB 大页映射数组,512 个 4KB "页"只占一个 TLB 项,就测不出 TLB 大小了。 程序用 `madvise(MADV_NOHUGEPAGE)` 禁止大页。 - **缓存组冲突**:步长正好 4KB 时,所有访问落在 L1 缓存的同一个组里(L1 是 8 路组相联),超过 8 页就开始缓存未命中, 和 TLB 效应混在一起。程序让第 p 页访问第 (p mod 64) 个缓存行来错开(`-n` 可关闭,自己对比一下)。 6. **指针追逐模式 `-p`**:原书的循环里各次访问互相独立,乱序执行 CPU 能同时处理多个未命中,把延迟藏起来。 指针追逐模式让每次访问的地址取决于上一次读出的值,测到的是真正的延迟,拐点清楚得多。 ## 示例结果(AMD Ryzen 7 8745H / Zen 4,WSL2,gcc 14) 你的机器上数字会不同,WSL2 下还有噪声,这里取 7 次测量的最小值: | 页数 | a[i]+=1 (ns) | 指针追逐 (ns) | 解释 | |---:|---:|---:|---| | 1–64 | 0.32–0.47 | 1.26 | L1 TLB 命中 + L1 缓存命中 | | 96–512 | 0.43–0.57 | 2.8 | 超过 L1 DTLB(72 项),第二级 TLB 命中 | | 768–2048 | 0.53–0.56 | 5.2 | 超过 L1 数据缓存(512 行 = 32KB),是缓存效应不是 TLB | | 3072 | 2.50 | 11.6 | 接近并超过 L2 DTLB(3072 项),开始遍历页表 | | 8192–24576 | 5.2–5.5 | 27–38 | 几乎每次都遍历页表,页表项和数据也挤出了缓存 | 要点:第一级 TLB 的拐点(64→96 页)在指针追逐里很清楚,在原书的循环里几乎看不到; 第二级 TLB 的拐点(2048→3072 页)两种方式都很明显。L1 TLB 命中和遍历页表之间差了 10~30 倍。