为什么要为其他逻辑处理器引起的 Memory Order Violation 冲洗管道?

Why flush the pipeline for Memory Order Violation caused by other logical processors?

Memory Order Machine Clear performance event is described by the vTune documentation为:

The memory ordering (MO) machine clear happens when a snoop request from another processor matches a source for a data operation in the pipeline. In this situation the pipeline is cleared before the loads and stores in progress are retired.

但是我不明白为什么会这样。不同逻辑处理器上的加载和存储之间没有同步顺序。
处理器可以假装窥探发生所有当前进行中的数据操作都提交之后。

问题也有描述here

A memory ordering machine clear gets triggered whenever the CPU core detects a “memory ordering conflict”. Basically, this means that some of the currently pending instructions tried to access memory that we just found out some other CPU core wrote to in the meantime. Since these instructions are still flagged as pending while the “this memory just got written” event means some other core successfully finished a write, the pending instructions – and everything that depends on their results – are, retroactively, incorrect: when we started executing these instructions, we were using a version of the memory contents that is now out of date. So we need to throw all that work out and do it over. That’s the machine clear.

但这对我来说毫无意义,CPU 不需要重新执行加载队列中的加载,因为没有非锁定 loads/stores 的总订单。

我发现一个问题是允许重新排序负载:

;foo is 0
mov eax, [foo]    ;inst 1
mov ebx, [foo]    ;inst 2
mov ecx, [foo]    ;inst 3

如果执行顺序是 1 3 2 那么像 mov [foo], 1 这样介于 3 和 2 之间的存储会导致

eax = 0
ebx = 1
ecx = 0

这确实违反了内存排序规则。

但是负载不能随负载重新排序,那么当来自另一个内核的侦听请求与任何运行中负载的来源相匹配时,为什么英特尔的 CPU 会刷新管道?
这种行为防止了哪些错误情况?

尽管 x86 内存排序模型不允许加载到除 WC 之外的任何内存类型以在程序顺序之外全局可见,但 实现 实际上允许加载完成命令。在所有先前的加载完成之前停止发出加载请求将是非常昂贵的。考虑以下示例:

load X
load Y
load Z

假设缓存层次结构中不存在第 x 行,必须从内存中获取。但是,Y 和 Z 都存在于 L1 缓存中。维护 x86 加载顺序要求的一种方法是在加载 X 获取数据之前不发出加载 Y 和 X。但是,这会停止所有依赖于 Y 和 Z 的指令,从而导致潜在的巨大性能损失。

文献中已经提出并广泛研究了多种解决方案。英特尔在其所有处理器中实施的方法允许乱序发出加载,然后检查是否发生了内存排序违规,在这种情况下,将重新发出违规加载并重播其所有相关指令。但这种违规只有在满足以下条件时才会发生:

  • 加载已完成,而程序顺序中的前一个加载仍在等待其数据,并且这两个加载是针对需要排序的内存类型。
  • 另一个物理或逻辑内核修改了后面的负载读取的行,并且在前面的负载获取数据之前发出负载的逻辑内核检测到此更改。

当这两种情况都发生时,逻辑核心检测到内存顺序冲突。考虑以下示例:

------           ------
core1            core2
------           ------
load rdx, [X]    store [Y], 1
load rbx, [Y]    store [X], 2
add  rdx, rbx
call printf

假设初始状态为:

  • [X] = [Y] = 0.
  • 包含Y的缓存行已经存在于core1的L1D中。但是 X 不存在于 core1 的私有缓存中。
  • 线 X 以可修改的一致性状态存在于 core2 的 L1D 中,线 Y 以可共享状态存在于 core2 的 L1D 中。

根据x86强排序模型,唯一可能的合法结果是0、1和3。特别地,结果2是不合法的。

可能会发生以下事件序列:

  • Core2 为两条线路发出 RFO。 X 行的 RFO 将很快完成,但 Y 行的 RFO 必须一直到 L3 才能使 core1 的私有缓存中的行无效。请注意,core2 只能按顺序提交存储,因此第 X 行的存储要等到第 Y 行的存储提交。
  • Core1 将两个负载下发给L1D。来自 Y 行的加载很快完成,但是来自 X 的加载需要从 core2 的私有缓存中获取该行。请注意,此时 Y 的值为零。
  • 第 Y 行从 core1 的私有缓存中失效,它在 core2 中的状态更改为可修改的一致性状态。
  • Core2 现在按顺序提交两个存储。
  • X 线从 core2 转发到 core1。
  • Core1 从缓存行 X 加载 core2 存储的值,即 2。
  • Core1 打印 X 和 Y 之和,即 0 + 2 = 2。这是非法结果。本质上,core1 加载了 Y 的陈旧值。

为了保持加载顺序,core1 的加载缓冲区必须侦听驻留在其专用缓存中的行的所有无效。当它检测到 Y 行已失效,而在程序顺序中从失效行完成的加载之前有挂起的加载时,就会发生内存顺序冲突,并且必须重新发出加载,然后它才能获得最新值。请注意,如果 Y 行在失效之前和 X 的加载完成之前已从 core1 的私有缓存中逐出,它可能无法首先侦听 Y 行的失效。所以也需要有一种机制来处理这种情况。

如果 core1 从不使用一个或两个加载的值,加载顺序违规可能 发生 ,但永远不会 观察到 .类似地,如果 core2 存储到行 X 和 Y 的值相同,则可能会发生加载顺序违规,但无法观察到。然而,即使在这些情况下,core1 仍然会不必要地重新发出违规负载并重播其所有依赖项。