测量 C/C++ 中页表锁的开销

Moh*_*shi 1 c++ linux multithreading locks

Linux内核中页表的PMD和PTE级别有两个锁。每次线程/进程分配/映射内存时,它都应该持有这些锁之一以相应地更新页表。显然,随着线程数量的增加,持有锁的竞争也会增加。这可能会降低内存映射吞吐量,因为许多线程持有自旋锁。

我想要测量的任务是这些锁对内存映射吞吐量的最坏情况开销,但我不知道如何测量它。

我尝试在增加运行同一循环的线程数时使用mallocin an 。infinite-loop我检查/proc/{pid}/maps每组正在运行的线程以计算映射内存的数量。但我不确定这是否是正确的方法。此外,这种方法会消耗大量内存。

有没有有效的方法来衡量这些锁的最坏情况开销?

小智 5

很多评论都是正确的,但我想我可以尝试完整的回应。首先,使用 malloc 不会使您能够对页面映射进行显式控制,因为注释表示 stdlib 的 malloc 部分实际上会在第一次分配后分配一大块内存。其次,当创建新线程时,这将使用相同的地址空间,因此不会创建额外的映射。

我假设您想从用户空间执行此操作,因为从内核空间,您可以做很多事情来使这种探索有些退化(例如,您可以尝试将页面映射到同一位置)。相反,您想使用 mmap 分配匿名页面。mmap 是创建虚拟内存条目的显式调用,以便当第一次访问该特定页面时,内核实际上可以在该位置放置一些空白物理内存。第一次访问该位置会导致故障,并且第一次访问实际上会使用 PTE 和 PUD 中的锁。

确保良好的基准测试程序:

  1. 如果您只是想对页表施加压力,您可能还想在该过程中关闭透明大页。(要查看的系统调用是带有 DISABLE_THP 标志的 prnctl)。在生成任何子进程之前运行此命令。
  2. 使用 cpuset 将线程固定到核心。
  3. 您想要显式控制您感兴趣的区域,因此您需要为共享相同页表的每个线程选择特定地址。这样可以确保使用最大数量的锁。
  4. 使用伪随机函数写入每个线程具有不同种子的位置。
  5. 与执行完全相同的操作但受压力的地址空间部分截然不同的基线进行比较。
  6. 确保基线和过度满足的工作负载之间的差异尽可能小。
  7. 不要过度订阅处理器,这会产生由于上下文切换而导致的开销,而上下文切换是众所周知的无法根除的。
  8. 确保在创建线程后开始捕获计时,并在线程被销毁之前停止它。

这在每个线程中意味着什么:

address = <per-thread address>
total = 0;
for(int i = 0; i < N; i++) 
{
 uint64_t* x = (uint64_t*) mmap((void*) address, 4096, PROT_READ | PROT_WRITE, 
     MAP_ANONYMOUS, -1, 0); //Maps one page anonymously
 assert(x);
 *x ^= pseudo_rand(); // Accesses the page and causes the allocation
 total += *x; // For fun
 int res = munmap((void*) x, 4096); //Deallocates the page (similar locks)
 assert(!res);
}
Run Code Online (Sandbox Code Playgroud)

最大的收获是:

  1. 使用mmap并显式访问分配的位置来实际控制单个页面分配。
  2. 地址的紧凑性决定了获取哪些锁。
  3. 测量内核和虚拟内存需要严格遵守基准测试程序。

  • 我不会像这样重复调用“mmap”不会分配单独的“vm_area_struct”,而是会扩展现有的“vm_area_struct”。这就是为什么“munmap”[可能会失败](/sf/ask/3062048881/)与“ENOMEM”:释放可能会导致`vm_area_struct` 的分割可以超过 `vm.max_map_count`。可能不会影响锁定,但对于了解基准测试可能很有用。 (2认同)