了解内存控制器 RPQ/WPQ 加载和 ntstore 的排序保证

问题描述 投票:0回答:1

我试图了解当 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 相对空虚
  • 问题加载,然后 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 与 WPQ ~18),什么机制阻止存储在其之前的加载之前完成?我怀疑除了简单的队列动态之外,一定还有某种排序机制,但我不明白它是什么。

这是完整的代码:https://gist.github.com/VinayBanakar/6841e553d274fa5b8a156c13937405c8

assembly x86 x86-64 cpu-architecture memory-model
1个回答
0
投票

在 x86 上(与 ARM 和其他平台不同),我非常确定负载无法退出(变得非推测性并允许稍后的 insns 也退出),直到返回值。 这可以让 CPU 捕获内存顺序错误推测,因为硬件实际上会提前加载并检查 LoadLoad 顺序违规等内容,并在必要时破坏管道。 (

machine_clears.memory_ordering
性能事件)。

弱序 ISA 不需要这样做;一旦知道负载没有故障并且请求已发送,他们就可以让负载从 ROB(重新排序缓冲区)中退出。 因此它仅由加载缓冲区条目跟踪,而不是 ROB 条目。

存储无法从存储缓冲区提交到 LFB 或 L1d 缓存,直到它变得非推测性为止,否则可能会使错误推测的存储值对其他核心可见,而无法回滚。 (例如,在检测到较早的分支错误预测或错误指令后。)

因此,x86 乱序执行硬件从根本上无法进行 LoadStore 重新排序,即使是弱排序的 NT 存储也是如此。 您对内存控制器的非核心请求无法立即执行。


SSE4.1

movntdqa
来自 WC 内存的加载是弱排序的(与来自任何其他内存类型的
movntdqa
加载不同 - 与 NT 存储不同,指令不会覆盖排序语义)。 假设,它们可以被允许在数据到达之前(在响应非核心请求之前)退出。 这不会违反内存模型,因为它们可以通过更早或更晚的加载和存储自由地重新排序(我认为),并且可能需要一个完整的
mfence
来阻止它们的重新排序。 我不知道是否有真正的 CPU 让它们在数据仍在传输时退出,或者它们是否都像正常负载一样处理它们,除了不检查内存顺序错误推测。 我猜是后者,因为这是一个非常罕见的用例,而且好处可能很小。

但是您正在使用普通的

mov
加载普通全局变量,这些变量将位于 WB(回写)内存区域中,因此这些都不适用于您的测试用例。

© www.soinside.com 2019 - 2024. All rights reserved.