加载-> 存储重新排序如何通过有序提交实现?
How is load->store reordering possible with in-order commit?
ARM 允许对后续存储的负载进行重新排序,因此以下伪代码:
// CPU 0 | // CPU 1
temp0 = x; | temp1 = y;
y = 1; | x = 1;
可以导致 temp0 == temp1 == 1
(而且,这在实践中也是可以观察到的)。我无法理解这是如何发生的;似乎按顺序提交会阻止它(据我所知,几乎所有 OOO 处理器中都存在这种情况)。我的推理是 "the load must have its value before it commits, it commits before the store, and the store's value can't become visible to other processors until it commits."
我猜我的一个假设一定是错误的,并且必须满足以下条件之一:
指令不需要按顺序一路提交。稍后的存储可以安全地提交并在较早的加载之前变得可见,只要在存储提交时核心可以保证先前的加载(和所有中间指令)不会触发异常,并且加载的地址是保证与商店的不同。
负载可以在其值已知之前提交。我不知道这将如何实施。
存储可以在提交之前变为可见。也许某个地方的内存缓冲区被允许将存储转发到不同的线程,即使负载更早排队?
完全是别的东西?
有很多假设的微架构特征可以解释这种行为,但我最好奇的是现代弱序 CPU 中实际存在的特征。
你的假设要点在我看来都是正确的,除了你可以构建一个 uarch,其中负载可以在仅检查负载的权限 (TLB) 以确保它肯定会发生之后从 OoO 核心退出。可能有 OoO exec CPU 执行此操作(更新:显然有)。
我认为 x86 CPUs 需要加载才能真正让数据在它们退出之前到达,但它们强大的内存模型无论如何都不允许 LoadStore 重新排序。所以 ARM 肯定会有所不同。
您说得对,商店在退役前不能对任何其他核心可见。那就是疯狂。即使在 SMT core(一个物理核心上的多个逻辑线程)上,它也会 link 一起推测两个逻辑线程,如果其中一个检测到错误推测,则要求它们都回滚。这将破坏 SMT 让一个逻辑线程利用其他逻辑线程的目的。
(相关:使退休但尚未提交(对 L1d)的存储对同一核心上的其他逻辑线程可见是一些真正的 PowerPC 实现如何使线程可能不同意存储的全局顺序。)
CPUs 按顺序执行可以启动加载(检查 TLB 并写入加载缓冲区条目)并且仅在指令尝试使用结果之前停止准备好。那么后面的指令,包括存储,都可以运行正常。这基本上是有序管道中非糟糕性能所必需的;在每次高速缓存未命中(甚至只是 L1d 延迟)时停止都是不可接受的。即使在 CPUs 上,内存并行也是一回事;它们可以有多个加载缓冲区来跟踪多个未完成的缓存未命中。像 Cortex-A53 这样的高性能有序 ARM 内核仍在现代智能手机中广泛使用,并且在使用结果寄存器之前调度加载是众所周知的对数组循环的重要优化。 (展开甚至软件流水线。)
因此,如果加载在缓存中未命中但存储命中(并在较早的缓存未命中加载获取数据之前提交到 L1d),您可以获得 LoadStore 重新排序。 (Jeff Preshing intro to memory reording 将该示例用于 LoadStore,但根本没有涉及 uarch 的详细信息。)
在检查了 TLB 和/或任何内存区域内容后,负载不会出错。该部分必须在退出之前完成,或者在到达有序管道的末端之前完成。就像停用存储在存储缓冲区中等待提交一样,停用加载在加载缓冲区中肯定会在某个时刻发生。
所以有序管道上的序列是:
lw r0, [r1]
TLB 命中,但未命中 L1d 缓存。加载执行单元将地址(r1
)写入加载缓冲区。任何试图读取 r0
的后续指令都会停止,但我们可以肯定地知道负载没有故障。
由于 r0
等待加载缓冲区准备就绪,lw
指令本身可以离开流水线(退出),后面的指令也可以。
任意数量的不读取 r0 的其他指令。这将使有序管道停滞。
sw r2, [r3]
存储执行单元将地址+数据写入存储缓冲区/队列。那么这条指令就可以退休了。
探测加载缓冲区发现此存储与挂起的加载不重叠,因此它可以提交到 L1d。(如果它 有 重叠,无论如何,在 MESI RFO 完成之前你不能提交它,并且快速重启会将传入的数据转发到加载缓冲区。因此处理这种情况可能不会太复杂,甚至不需要对每个存储进行探测,但是让我们只看单独缓存行的情况,我们可以在其中获得 LoadStore 重新排序)
致力于 L1d = 全球可见。当较早的加载仍在等待缓存行到达时,可能会发生这种情况。
对于 OoO CPUs,您需要一些方法将加载完成绑定回 OoO 核心以等待加载结果的指令。我想这是可能的,但这意味着寄存器的 architectural/retirement 值可能不会存储在核心中的任何地方。错误推测导致的流水线刷新和其他回滚必须依赖传入负载与物理和架构寄存器之间的关联。 (不过,在管道回滚时不刷新存储缓冲区已经是 CPUs 必须做的事情。位于存储缓冲区中的已退役但尚未提交的存储无法回滚。)
对于具有小 OoO window 的 uarches 来说,这可能是一个很好的设计理念,OoO 太小以至于无法隐藏缓存未命中。 (公平地说,每个高性能 OoO 执行者都是如此 CPU:内存延迟通常太高而无法完全隐藏。)
我们有 LoadStore 在 OoO ARM 上重新排序的实验证据:https://www.cl.cam.ac.uk/~pes20/ppc-supplemental/test7.pdf shows non-zero counts for "load buffering" on Tegra 2, which is based on the out-of-order Cortex-A9 uarch 的第 7.1 节。我没有查找所有其他人,但我确实重写了答案以表明这也是无序 CPUs 的可能机制。不过,我不确定情况是否如此。
ARM 允许对后续存储的负载进行重新排序,因此以下伪代码:
// CPU 0 | // CPU 1
temp0 = x; | temp1 = y;
y = 1; | x = 1;
可以导致 temp0 == temp1 == 1
(而且,这在实践中也是可以观察到的)。我无法理解这是如何发生的;似乎按顺序提交会阻止它(据我所知,几乎所有 OOO 处理器中都存在这种情况)。我的推理是 "the load must have its value before it commits, it commits before the store, and the store's value can't become visible to other processors until it commits."
我猜我的一个假设一定是错误的,并且必须满足以下条件之一:
指令不需要按顺序一路提交。稍后的存储可以安全地提交并在较早的加载之前变得可见,只要在存储提交时核心可以保证先前的加载(和所有中间指令)不会触发异常,并且加载的地址是保证与商店的不同。
负载可以在其值已知之前提交。我不知道这将如何实施。
存储可以在提交之前变为可见。也许某个地方的内存缓冲区被允许将存储转发到不同的线程,即使负载更早排队?
完全是别的东西?
有很多假设的微架构特征可以解释这种行为,但我最好奇的是现代弱序 CPU 中实际存在的特征。
你的假设要点在我看来都是正确的,除了你可以构建一个 uarch,其中负载可以在仅检查负载的权限 (TLB) 以确保它肯定会发生之后从 OoO 核心退出。可能有 OoO exec CPU 执行此操作(更新:显然有)。
我认为 x86 CPUs 需要加载才能真正让数据在它们退出之前到达,但它们强大的内存模型无论如何都不允许 LoadStore 重新排序。所以 ARM 肯定会有所不同。
您说得对,商店在退役前不能对任何其他核心可见。那就是疯狂。即使在 SMT core(一个物理核心上的多个逻辑线程)上,它也会 link 一起推测两个逻辑线程,如果其中一个检测到错误推测,则要求它们都回滚。这将破坏 SMT 让一个逻辑线程利用其他逻辑线程的目的。
(相关:使退休但尚未提交(对 L1d)的存储对同一核心上的其他逻辑线程可见是一些真正的 PowerPC 实现如何使线程可能不同意存储的全局顺序。
CPUs 按顺序执行可以启动加载(检查 TLB 并写入加载缓冲区条目)并且仅在指令尝试使用结果之前停止准备好。那么后面的指令,包括存储,都可以运行正常。这基本上是有序管道中非糟糕性能所必需的;在每次高速缓存未命中(甚至只是 L1d 延迟)时停止都是不可接受的。即使在 CPUs 上,内存并行也是一回事;它们可以有多个加载缓冲区来跟踪多个未完成的缓存未命中。像 Cortex-A53 这样的高性能有序 ARM 内核仍在现代智能手机中广泛使用,并且在使用结果寄存器之前调度加载是众所周知的对数组循环的重要优化。 (展开甚至软件流水线。)
因此,如果加载在缓存中未命中但存储命中(并在较早的缓存未命中加载获取数据之前提交到 L1d),您可以获得 LoadStore 重新排序。 (Jeff Preshing intro to memory reording 将该示例用于 LoadStore,但根本没有涉及 uarch 的详细信息。)
在检查了 TLB 和/或任何内存区域内容后,负载不会出错。该部分必须在退出之前完成,或者在到达有序管道的末端之前完成。就像停用存储在存储缓冲区中等待提交一样,停用加载在加载缓冲区中肯定会在某个时刻发生。
所以有序管道上的序列是:
lw r0, [r1]
TLB 命中,但未命中 L1d 缓存。加载执行单元将地址(r1
)写入加载缓冲区。任何试图读取r0
的后续指令都会停止,但我们可以肯定地知道负载没有故障。由于
r0
等待加载缓冲区准备就绪,lw
指令本身可以离开流水线(退出),后面的指令也可以。任意数量的不读取 r0 的其他指令。这将使有序管道停滞。
sw r2, [r3]
存储执行单元将地址+数据写入存储缓冲区/队列。那么这条指令就可以退休了。探测加载缓冲区发现此存储与挂起的加载不重叠,因此它可以提交到 L1d。(如果它 有 重叠,无论如何,在 MESI RFO 完成之前你不能提交它,并且快速重启会将传入的数据转发到加载缓冲区。因此处理这种情况可能不会太复杂,甚至不需要对每个存储进行探测,但是让我们只看单独缓存行的情况,我们可以在其中获得 LoadStore 重新排序)
致力于 L1d = 全球可见。当较早的加载仍在等待缓存行到达时,可能会发生这种情况。
对于 OoO CPUs,您需要一些方法将加载完成绑定回 OoO 核心以等待加载结果的指令。我想这是可能的,但这意味着寄存器的 architectural/retirement 值可能不会存储在核心中的任何地方。错误推测导致的流水线刷新和其他回滚必须依赖传入负载与物理和架构寄存器之间的关联。 (不过,在管道回滚时不刷新存储缓冲区已经是 CPUs 必须做的事情。位于存储缓冲区中的已退役但尚未提交的存储无法回滚。)
对于具有小 OoO window 的 uarches 来说,这可能是一个很好的设计理念,OoO 太小以至于无法隐藏缓存未命中。 (公平地说,每个高性能 OoO 执行者都是如此 CPU:内存延迟通常太高而无法完全隐藏。)
我们有 LoadStore 在 OoO ARM 上重新排序的实验证据:https://www.cl.cam.ac.uk/~pes20/ppc-supplemental/test7.pdf shows non-zero counts for "load buffering" on Tegra 2, which is based on the out-of-order Cortex-A9 uarch 的第 7.1 节。我没有查找所有其他人,但我确实重写了答案以表明这也是无序 CPUs 的可能机制。不过,我不确定情况是否如此。