【问题标题】:Regarding instruction ordering in executions of cache-miss loads before cache-hit stores on x86关于在 x86 上缓存命中存储之前执行缓存未命中加载的指令顺序
【发布时间】:2019-10-02 01:43:46
【问题描述】:

鉴于如下所示的小程序(从顺序一致性/TSO 的角度来看,手工制作看起来相同),并假设它由超标量乱序 x86 cpu 运行:

Load A <-- A in main memory
Load B <-- B is in L2
Store C, 123 <-- C is L1

我有几个问题:

  1. 假设指令窗口足够大,三个指令会同时被获取、解码和执行吗?我认为不会,因为这会破坏程序顺序的执行。
  2. 第二次加载从内存中获取 A 比 B 花费更长的时间。后者是否必须等到第一次完全执行?是否会在 Load A 完全执行后才开始获取 B?还是要等到什么时候?
  3. 为什么商店必须等待装载?如果是,该指令是等待在存储缓冲区中提交,直到加载完成,还是在解码后它必须坐下来等待加载?

谢谢

【问题讨论】:

  • 前端无关。 3 条连续的指令很可能在同一个 16 字节的取指块中被取指,并且可能作为一个组在同一个周期内进行预解码和解码。并且(也或相反)作为一组 3 或 4 个微指令的一部分发送到无序后端。 IDK 为什么您认为其中任何一个都会导致任何潜在问题。 (同时执行不会将后面的指令放在之前的指令之前,而是将它们放在同时时间。这很好。)
  • 2) 呢?
  • 我正在为那部分写一个答案;我只是评论指出你的假设中的缺陷。
  • 哦,谢谢! 654321
  • 您是否假设像 Pentium 或 Atom 这样的有序超标量管道?你从来没有提到乱序执行,只是超标量。因为只有有序流水线通常会一起执行一组一起解码。

标签: x86 cpu-architecture memory-model


【解决方案1】:

术语:“instruction-window”通常表示乱序执行窗口,CPU 可以在该窗口上找到 ILP。即 ROB 或 RS 大小。见Understanding the impact of lfence on a loop with two long dependency chains, for increasing lengths

在一个周期内有多少条指令可以通过流水线的术语是流水线宽度。例如Skylake 是 4 宽的超标量乱序。 (它的部分管道,如 decode、uop-cache fetch 和退休,比 4 uop 更宽,但 issue/rename 是最窄的点。)


术语:“等待在存储缓冲区中提交”存储数据+地址在存储执行时被写入存储缓冲区。它提交 存储缓冲区到L1d在退休后的任何时候,当它被认为是非推测性的。

(按照程序顺序,保持 TSO 内存模型没有存储重新排序。存储缓冲区允许存储在此核心内无序执行,但仍提交到 L1d(并变为全局可见)有序。 执行存储 = 将地址 + 数据写入存储缓冲区。
Can a speculatively executed CPU branch contain opcodes that access RAM?
还有what is a store buffer?
Size of store buffers on Intel hardware? What exactly is a store buffer?


前端无关。 3 条连续的指令很可能在同一个 16 字节的取指块中被取指,并且可能作为一个组在同一个周期内进行预解码和解码。并且(也或相反)作为一组 3 或 4 个微指令的一部分发送到无序后端。 IDK 为什么您认为其中任何一个都会导致任何潜在问题。

前端(从获取到发布/重命名)按程序顺序处理指令。同时处理不会将后面的指令放在之前的指令之前,而是将它们放在同时时间。更重要的是,它保留了节目顺序的信息;这不会丢失或丢弃,因为它对于依赖于前一个指令的指令很重要1

大多数流水线阶段之间存在队列,因此(例如在英特尔 Sandybridge 上)作为一组最多 6 条指令的一部分进行预解码的指令可能不会作为同一组 up- 的一部分到达解码器to-4(或更多与宏融合)。获取请参见https://www.realworldtech.com/sandy-bridge/3/,解码请参见下一页。 (还有 uop 缓存。)


执行(从乱序调度程序将微指令分派到执行端口)是排序问题所在。 乱序调度器必须避免破坏单线程代码。2

通常问题/重命名远远领先于执行,除非您在前端遇到瓶颈。所以通常没有理由期望一起发布的微指令会一起执行。 (为了争论,我们假设您显示的 2 个负载确实在同一个周期中被调度执行,无论它们是如何通过前端到达那里的。)

但无论如何,这里开始加载和存储都没有问题。 uop 调度程序不知道负载是否会在 L1d 中命中或丢失。它只是在一个周期内向加载执行单元发送 2 个加载微指令,并向这些端口发送一个存储地址 + 存储数据微指令。

  1. [加载顺序]

这是棘手的部分。

正如我在your last question 上的答案 + cmets 中解释的那样,现代 x86 CPU 将推测性地使用来自 Load B 的 L2 命中结果用于以后的指令,即使内存模型要求发生这种负载加载 A 之后。

但如果在加载 A 完成之前没有其他内核写入缓存行 B,则没有什么可以区分。内存顺序缓冲区负责检测之前加载的缓存行的失效较早的加载完成,并在允许加载重新排序可能会改变结果的极少数情况下执行内存顺序错误推测管道刷新(回滚到退休状态)。

  1. 为什么商店必须等待加载?

不会,除非存储地址取决于加载值。 uop 调度程序将在它们的输入准备好时将存储地址和存储数据 uop 分派到执行单元。

在程序顺序加载之后,就全局内存顺序而言,存储缓冲区会在加载后更远。存储缓冲区不会将存储数据提交到L1d(使其全局可见)直到商店退休后。既然是在负载之后,他们也会退休。

(退休是有序的以允许精确的异常,并确保没有先前的指令发生异常或错误预测的分支。按顺序退休允许我们可以肯定地说,一条指令在退休后是非投机性的。)

所以是的,这种机制确实确保了在两次加载都从内存中获取数据之前,存储不能提交到 L1d(通过 L1d 缓存,它为所有内核提供一致的内存视图)。因此,这可以防止 LoadStore 重新排序(较早的加载与较晚的存储)。

我不确定是否有弱排序的 OoO CPU 进行 LoadStore 重新排序。当缓存未命中加载发生在缓存命中存储之前时,在有序 CPU 上是可能的,并且 CPU 使用记分板来避免停滞,直到加载数据实际从寄存器中读取(如果仍未准备好)。 (LoadStore 是一个奇怪的东西:另见 Jeff Preshing 的 Memory Barriers Are Like Source Control Operations)。也许一些 OoO exec CPU 也可以跟踪缓存未命中存储在退休后,当它们被知道肯定会发生时,但数据还没有到达。 x86 不这样做,因为它会违反 TSO 内存模型。


脚注 1: 在某些架构(通常是 VLIW)中,同时指令的捆绑以对软件可见的方式成为架构的一部分。因此,如果软件不能用可以同时执行的指令填充所有 3 个插槽,它必须用 NOP 填充它们。甚至可能允许将 2 个寄存器与包含 mov r0, r1mov r1, r0 的包交换,具体取决于 ISA 是否允许同一包中的指令读取和写入相同的寄存器。

但 x86 并非如此:超标量乱序执行必须始终保持按照程序顺序一次运行一条指令的错觉。 OoO exec 的基本规则是:不要破坏单线程代码。

任何违反此规定的行为都只能使用checking for hazards 来完成,或者推测性地在检测到错误时回滚。

脚注 2:(接脚注 1)

您可以获取/解码/发出两条背靠背的inc eax 指令,但它们不能在同一个周期内执行,因为寄存器重命名 + OoO 调度程序必须检测到第二条读取首先。

【讨论】:

  • 啊!现在很清楚了。尤其是关于尽快运行负载但仍然从缓存中监听行失效的部分,这是我不知道的部分。
猜你喜欢
  • 1970-01-01
  • 2015-08-27
  • 2012-04-21
  • 2014-06-13
  • 2015-07-09
  • 2020-10-06
  • 1970-01-01
  • 2016-08-02
相关资源
最近更新 更多