每个 mmap/access/munmap 两次 TLB 未命中

Two TLB-miss per mmap/access/munmap

for (int i = 0; i < 100000; ++i) {
    int *page = mmap(NULL, PAGE_SIZE, PROT_READ | PROT_WRITE,
                            MAP_ANONYMOUS | MAP_PRIVATE, -1, 0);

    page[0] = 0;

    munmap(page, PAGE_SIZE);
}

我希望在用户空间中得到约 100000 个 dTLB 存储未命中,每次迭代一个(还有约 100000 个页面错误和内核的 dTLB 加载未命中)。 运行 按照命令,结果大约是我预期的 2 倍。如果有人能澄清为什么会这样,我将不胜感激:

perf stat -e dTLB-store-misses:u ./test
Performance counter stats for './test':

           200,114      dTLB-store-misses

       0.213379649 seconds time elapsed

P.S。我已经验证并确定生成的代码不会引入任何可以证明此结果合理的内容。另外,我确实得到了 ~100000 个页面错误和 dTLB-load-misses:k.

更新2:我认为Brendan 的回答是正确的。我也许应该删除它,但我认为 ocperf.py 建议对未来的读者仍然有用。并且它可以解释 CPUs 上额外的 TLB 未命中,而没有带有减轻 Meltdown 的内核的 Process-Context-Identifiers。

更新:以下猜测错误。新的猜测:mmap 必须修改您进程的页面 table,因此可能有一些 TLB 使某些东西失效。我建议使用 ocperf.py record 来尝试找出 哪个 asm 指令导致 TLB 未命中仍然有效。即使启用了优化,当 pushing/popping 一个 return glibc 包装函数调用的地址时,代码也会存储到堆栈中。


也许你的内核有 kernel / user page-table isolation enabled to mitigate Meltdown,所以从内核到用户 return,所有 TLB 条目都已失效(通过修改 CR3 指向页面 table根本不包括内核映射)。

在您的 dmesg 输出中查找 Kernel/User page tables isolation: enabled。如果您不介意在测试时容易受到 Meltdown 攻击,您可以尝试使用 kpti=off 作为内核选项来禁用它。


因为您使用的是 C,所以您是通过它们的 glibc 包装器使用 mmapmunmap 系统调用,而不是直接使用内联 syscall 指令。该包装器中的 ret 指令需要从堆栈中加载 return 地址,TLB 未命中该地址。

额外的存储未命中可能来自 call 指令推送 return 地址,尽管我不确定这是正确的,因为当前堆栈页面应该已经在 [=] 的 TLB 中18=] 来自上一个系统调用。


您可以使用 ocperf.py to get symbolic names for uarch-specific events 配置文件。假设您在最近的 Intel CPU、ocperf.py record -e mem_inst_retired.stlb_miss_stores,page-faults,dTLB-load-misses 上查找导致存储未命中的指令。 (然后使用 ocperf.py report -Mintel)。如果 report 不容易选择要查看的事件,请只记录一个事件。

mem_inst_retired.stlb_miss_stores 是一个 "precise" 事件,与大多数其他存储 TLB 事件不同,因此计数应该针对实际指令,而不是一些稍后的指令,如不精确的 perf 事件。 (请参阅 Andy Glew's trap vs. exception answer 以了解有关为何某些性能计数器无法轻易精确的详细信息;许多商店事件并非如此。)


I expect to get ~100000 dTLB-store-misses in userspace, one per each iteration

我希望:

  • CPU 尝试执行 page[0] = 0;,尝试加载包含 page[0] 的缓存行,找不到它的 TLB 条目,递增 dTLB-load-misses,获取翻译,意识到页面是"not present",然后产生页面错误。
  • 页面错误处理程序分配一个页面并且(因为页面 table 被修改)确保 TLB 条目无效(可能是因为英特尔 CPU 没有缓存 "not present" 页面,不一定通过显式执行 INVLPG)。页面错误处理程序 returns 到导致错误的指令,以便可以重试。
  • CPU 尝试第二次执行 page[0] = 0;,尝试加载包含 page[0] 的缓存行,找不到它的 TLB 条目,递增 dTLB-load-misses, 获取翻译,然后修改缓存行。

为了好玩,您可以使用 MAP_POPULATE 标志和 mmap() 来尝试让内核预分配页面(并避免页面错误和第一个 TLB 未命中)。