10

vTune 文档将Memory Order Machine Clear 性能事件描述为:

当来自另一个处理器的窥探请求与管道中数据操作的源匹配时,会发生内存排序 (MO) 机器清除。在这种情况下,管道在正在进行的加载和存储退出之前被清除。

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

这里也描述了这个问题

每当 CPU 内核检测到“内存排序冲突”时,就会触发内存排序机器清除。基本上,这意味着一些当前未决的指令试图访问我们刚刚发现其他一些 CPU 内核同时写入的内存。由于这些指令仍被标记为待处理,而“此内存刚刚被写入”事件意味着其他内核成功完成了写入,待处理的指令——以及依赖于它们结果的所有内容——追溯起来是不正确的:当我们开始执行这些指令时说明,我们使用的内存内容版本现在已经过时了。所以我们需要把所有的工作都扔掉,然后再做一遍。那是机器清楚的。

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

我可以看到一个问题是允许重新排序负载:

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

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

eax = 0
ebx = 1
ecx = 0

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

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

4

1 回答 1

12

尽管 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 仍会不必要地重新发出违规负载并重放其所有依赖项。

于 2019-04-07T20:28:17.770 回答