我试图了解当 RPQ(读取待处理队列)和 WPQ(写入待处理队列)之间存在显著的队列压力差异时,内存控制器如何在非临时负载和非临时存储之间维持程序顺序。
考虑以下序列:
load A // goes to RPQ
ntstore A // goes to WPQ
如果 RPQ 有许多待处理的条目(比如 20 个)并且 WPQ 相对空(比如 2 个条目),直观地看似乎存储在加载完成之前就可以到达 DRAM:
- 加载 A 在 RPQ 中排在另外 20 个读取之后
- ntstore A 进入几乎为空的 WPQ 并可以快速完成
- 这会破坏程序顺序,因为存储会在之前的加载之前完成
我编写了一个测试程序(见下文)来验证这个假设。其中:
- 造成较大的 RPQ 压力,同时保持 WPQ 相对空闲
- 发出 load 请求,然后发出 ntstore 请求到同一地址
- 检查 load 是否看到 ntstore 写入的值(这表明顺序违规)
- 通过将 RPQ 填充地址与测试地址分开来防止缓存效应
核心测试序列:
uint64_t val = MAGIC_A;
uint64_t addr = (uint64_t)&memory[test_idx].sentinel;
asm volatile(
"mov (%1), %%rax\n\t" // Load into rax
"movnti %%rbx, (%1)\n\t" // NT Store
"mov %%rax, %0\n\t" // Save loaded value
: "=r"(val)
: "r"(addr), "b"(MAGIC_B)
: "rax", "memory"
);
if (val == MAGIC_B) { // Would indicate store completed before load
local_violations++;
}
结果:以 16 个线程和每个线程 10M 缓存行运行,我们发现 0 个违规。性能计数器确认我们达到了所需的队列压力:
$ sudo perf stat -e uncore_imc_0/unc_m_rpq_occupancy/ -e uncore_imc_0/unc_m_rpq_inserts/ -e uncore_imc_0/unc_m_wpq_occupancy/ -e uncore_imc_0/unc_m_wpq_inserts/ -e uncore_imc_3/unc_m_rpq_occupancy/ -e uncore_imc_3/unc_m_rpq_inserts/ -e uncore_imc_3/unc_m_wpq_occupancy/ -e uncore_imc_3/unc_m_wpq_inserts/ sleep 60
[sudo] password for vin:
Performance counter stats for 'system wide':
2,893,410,795,007 uncore_imc_0/unc_m_rpq_occupancy/
9,443,033,953 uncore_imc_0/unc_m_rpq_inserts/
574,954,888,344 uncore_imc_0/unc_m_wpq_occupancy/
32,101,285 uncore_imc_0/unc_m_wpq_inserts/
1,086,622,871 uncore_imc_3/unc_m_rpq_occupancy/
38,269,189 uncore_imc_3/unc_m_rpq_inserts/
76,056,378,805 uncore_imc_3/unc_m_wpq_occupancy/
31,895,245 uncore_imc_3/unc_m_wpq_inserts/
60.002128565 seconds time elapsed
在这种情况下,内存控制器如何维持程序顺序?鉴于队列深度存在显著差异(RPQ ~306 vs WPQ ~18),哪些机制可以防止存储在其先前的加载之前完成?我怀疑除了简单的队列动态之外还存在某种排序机制,但我不明白它是什么。
以下是完整的代码: https: //gist.github.com/VinayBanakar/6841e553d274fa5b8a156c13937405c8