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