关于在 x86 上缓存命中存储之前执行缓存未命中加载的指令顺序
Regarding instruction ordering in executions of cache-miss loads before cache-hit stores on x86
鉴于下面显示的小程序(从顺序一致性/TSO 的角度来看,手工制作看起来相同),并假设它是 运行 超标量乱序 x86 cpu:
Load A <-- A in main memory
Load B <-- B is in L2
Store C, 123 <-- C is L1
我有几个问题:
- 假设一条足够大的指令-window,三个指令会同时取指、译码、执行吗?我假设不会,因为那样会中断程序顺序的执行。
- 第二次加载从内存中获取 A 比 B 花费的时间更长。后者是否必须等到第一次完全执行?是否只有在 Load A 完全执行后才开始获取 B?还是要等到什么时候?
- 为什么商店必须等待装货?如果是,该指令是等待在存储缓冲区中提交直到加载完成,还是在解码后必须坐下来等待加载?
谢谢
术语:“instruction-window”通常表示out-of-order执行window,CPU可以找到ILP。即 ROB 或 RS 尺寸。参见
一个周期内可以通过流水线的指令数的术语是流水线宽度。例如Skylake 是 4 宽超标量 out-of-order。 (它的部分管道,如解码、uop-cache 提取和退役,宽度超过 4 微指令,但 issue/rename 是最窄的点。)
术语:“等待在存储缓冲区中提交”存储数据+地址在存储执行时写入存储缓冲区。它 提交 从 存储缓冲区到 L1d 在退休后的任何时候,当它已知是 non-speculative.
(在程序顺序中,保持无存储重新排序的 TSO 内存模型。存储缓冲区允许存储在该核心内乱序执行执行,但仍然致力于 L1d(并变得全局可见)in-order。 执行存储 = 将地址 + 数据写入存储缓冲区。)
还有 what is a store buffer? 和
与front-end无关。 3 个连续的指令很可能在同一个 16 字节的提取块中被提取,并且可能作为一个组在相同的周期中经过 pre-decode 和解码。并且(同时或相反)作为 3 或 4 微指令组的一部分发布到 out-of-order back-end 中。 IDK 为什么你认为其中任何一个会导致任何潜在的问题。
前端(从fetch到issue/rename)按程序顺序处理指令。同时处理不会将后面的指令 放在 前面的指令之前,而是将它们放在 相同的 时间。更重要的是,它保留了程序顺序的信息;这不会丢失或丢弃,因为它对依赖于前一个指令的指令很重要1!
大多数流水线阶段之间都有队列,因此(例如在英特尔 Sandybridge 上)pre-decode 作为一组 up-to-6 指令的一部分的指令可能不会作为同一组up-to-4(或更多macro-fusion)。请参阅 https://www.realworldtech.com/sandy-bridge/3/ 进行提取,并在下一页进行解码。 (还有 uop 缓存。)
Executing(从 out-of-order 调度程序将 uops 调度到执行端口)是排序的关键所在。 out-of-order 调度程序必须避免破坏单线程代码。2
通常 issue/rename 远远领先于执行,除非您在 front-end 上遇到瓶颈。所以通常没有理由期望一起发出的微指令会一起执行。 (为了争论,我们假设您显示的 2 个负载确实在同一周期中被分派执行,无论它们是如何通过 front-end 到达那里的。)
但是无论如何,这里没有问题starting同时加载和存储。 uop 调度程序不知道加载是否会在 L1d 中命中或未命中。它只是在一个周期内向加载执行单元发送 2 个加载微指令,并向这些端口发送一个 store-address + store-data 微指令。
- [load ordering]
这是棘手的部分。
正如我在 的回答 + 评论中所解释的那样,现代 x86 CPUs 将 推测性地 使用加载 B 的 L2 命中结果供以后使用指令,即使内存模型要求此加载发生在加载 A 之后。
但是,如果在加载 A 完成之前没有其他核心写入缓存行 B,则没有任何区别。 Memory-Order 缓冲区负责检测无效在较早的加载完成之前加载的缓存行,并在允许加载 re-ordering 的极少数情况下执行 memory-order mis-speculation 管道刷新(回滚到退休状态)可能会更改结果。
- Why would the store have to wait for the loads?
不会,除非 store-address 取决于负载值。 uop 调度程序将调度 store-address 和 store-data当他们的输入准备好时,微指令到执行单元。
在程序顺序加载之后,就全局内存顺序而言,存储缓冲区将使其在加载之后更远。存储缓冲区不会将商店数据提交给 L1d(使其全局可见),直到商店退役。由于是在负荷之后,他们也将退休。
(退休是 in-order 以允许精确的异常,并确保没有 previous 指令发生异常或被一个错误预测的分支。In-order 退休让我们可以肯定地说一条指令在退休后是 non-speculative。)
所以是的,这种机制确实确保了在两个加载都从内存中获取数据之前,存储不能提交给 L1d(通过 L1d 缓存,它为所有内核提供一致的内存视图)。因此,这可以防止 LoadStore 重新排序(较早的加载和较晚的存储)。
我不确定是否有 weakly-ordered OoO CPU 做 LoadStor重新排序。在 in-order CPU 上,当 cache-miss 加载出现在 cache-hit 存储之前时,并且 CPU 使用记分板来避免停顿,直到加载数据实际上从寄存器中读取,如果它还没有准备好。 (LoadStore 很奇怪:另请参阅 Jeff Preshing 的 Memory Barriers Are Like Source Control Operations)。也许一些 OoO 执行官 CPUs 也可以跟踪 cache-miss 商店 post 退休,当他们知道他们肯定会发生时,但数据还没有到达。 x86 不这样做,因为它会违反 TSO 内存模型。
脚注 1: 有一些体系结构(通常是 VLIW),其中同步指令包以软件可见的方式成为体系结构的一部分。因此,如果软件无法用可同时执行的指令填充所有 3 个槽,则必须用 NOP 填充它们。甚至可能允许将 2 个寄存器与包含 mov r0, r1
和 mov r1, r0
的包交换,具体取决于 ISA 是否允许同一包中的指令读取和写入相同的寄存器。
但 x86 不是这样的:超标量 out-of-order 执行必须始终保持 运行 指令在程序顺序中一次一条的错觉。 OoO exec 的基本规则是:不要破坏 single-threaded 代码。
任何违反此规定的行为只能通过 checking for hazards 或推测性地在检测到错误后回滚来完成。
脚注 2:(接脚注 1)
您可以获取/解码/发出两条 back-to-back inc eax
指令,但它们不能在同一周期内执行,因为寄存器重命名 + OoO 调度程序必须检测到第二条读取第一个的输出。
鉴于下面显示的小程序(从顺序一致性/TSO 的角度来看,手工制作看起来相同),并假设它是 运行 超标量乱序 x86 cpu:
Load A <-- A in main memory
Load B <-- B is in L2
Store C, 123 <-- C is L1
我有几个问题:
- 假设一条足够大的指令-window,三个指令会同时取指、译码、执行吗?我假设不会,因为那样会中断程序顺序的执行。
- 第二次加载从内存中获取 A 比 B 花费的时间更长。后者是否必须等到第一次完全执行?是否只有在 Load A 完全执行后才开始获取 B?还是要等到什么时候?
- 为什么商店必须等待装货?如果是,该指令是等待在存储缓冲区中提交直到加载完成,还是在解码后必须坐下来等待加载?
谢谢
术语:“instruction-window”通常表示out-of-order执行window,CPU可以找到ILP。即 ROB 或 RS 尺寸。参见
一个周期内可以通过流水线的指令数的术语是流水线宽度。例如Skylake 是 4 宽超标量 out-of-order。 (它的部分管道,如解码、uop-cache 提取和退役,宽度超过 4 微指令,但 issue/rename 是最窄的点。)
术语:“等待在存储缓冲区中提交”存储数据+地址在存储执行时写入存储缓冲区。它 提交 从 存储缓冲区到 L1d 在退休后的任何时候,当它已知是 non-speculative.
(在程序顺序中,保持无存储重新排序的 TSO 内存模型。存储缓冲区允许存储在该核心内乱序执行执行,但仍然致力于 L1d(并变得全局可见)in-order。 执行存储 = 将地址 + 数据写入存储缓冲区。)
还有 what is a store buffer? 和
与front-end无关。 3 个连续的指令很可能在同一个 16 字节的提取块中被提取,并且可能作为一个组在相同的周期中经过 pre-decode 和解码。并且(同时或相反)作为 3 或 4 微指令组的一部分发布到 out-of-order back-end 中。 IDK 为什么你认为其中任何一个会导致任何潜在的问题。
前端(从fetch到issue/rename)按程序顺序处理指令。同时处理不会将后面的指令 放在 前面的指令之前,而是将它们放在 相同的 时间。更重要的是,它保留了程序顺序的信息;这不会丢失或丢弃,因为它对依赖于前一个指令的指令很重要1!
大多数流水线阶段之间都有队列,因此(例如在英特尔 Sandybridge 上)pre-decode 作为一组 up-to-6 指令的一部分的指令可能不会作为同一组up-to-4(或更多macro-fusion)。请参阅 https://www.realworldtech.com/sandy-bridge/3/ 进行提取,并在下一页进行解码。 (还有 uop 缓存。)
Executing(从 out-of-order 调度程序将 uops 调度到执行端口)是排序的关键所在。 out-of-order 调度程序必须避免破坏单线程代码。2
通常 issue/rename 远远领先于执行,除非您在 front-end 上遇到瓶颈。所以通常没有理由期望一起发出的微指令会一起执行。 (为了争论,我们假设您显示的 2 个负载确实在同一周期中被分派执行,无论它们是如何通过 front-end 到达那里的。)
但是无论如何,这里没有问题starting同时加载和存储。 uop 调度程序不知道加载是否会在 L1d 中命中或未命中。它只是在一个周期内向加载执行单元发送 2 个加载微指令,并向这些端口发送一个 store-address + store-data 微指令。
- [load ordering]
这是棘手的部分。
正如我在
但是,如果在加载 A 完成之前没有其他核心写入缓存行 B,则没有任何区别。 Memory-Order 缓冲区负责检测无效在较早的加载完成之前加载的缓存行,并在允许加载 re-ordering 的极少数情况下执行 memory-order mis-speculation 管道刷新(回滚到退休状态)可能会更改结果。
- Why would the store have to wait for the loads?
不会,除非 store-address 取决于负载值。 uop 调度程序将调度 store-address 和 store-data当他们的输入准备好时,微指令到执行单元。
在程序顺序加载之后,就全局内存顺序而言,存储缓冲区将使其在加载之后更远。存储缓冲区不会将商店数据提交给 L1d(使其全局可见),直到商店退役。由于是在负荷之后,他们也将退休。
(退休是 in-order 以允许精确的异常,并确保没有 previous 指令发生异常或被一个错误预测的分支。In-order 退休让我们可以肯定地说一条指令在退休后是 non-speculative。)
所以是的,这种机制确实确保了在两个加载都从内存中获取数据之前,存储不能提交给 L1d(通过 L1d 缓存,它为所有内核提供一致的内存视图)。因此,这可以防止 LoadStore 重新排序(较早的加载和较晚的存储)。
我不确定是否有 weakly-ordered OoO CPU 做 LoadStor重新排序。在 in-order CPU 上,当 cache-miss 加载出现在 cache-hit 存储之前时,并且 CPU 使用记分板来避免停顿,直到加载数据实际上从寄存器中读取,如果它还没有准备好。 (LoadStore 很奇怪:另请参阅 Jeff Preshing 的 Memory Barriers Are Like Source Control Operations)。也许一些 OoO 执行官 CPUs 也可以跟踪 cache-miss 商店 post 退休,当他们知道他们肯定会发生时,但数据还没有到达。 x86 不这样做,因为它会违反 TSO 内存模型。
脚注 1: 有一些体系结构(通常是 VLIW),其中同步指令包以软件可见的方式成为体系结构的一部分。因此,如果软件无法用可同时执行的指令填充所有 3 个槽,则必须用 NOP 填充它们。甚至可能允许将 2 个寄存器与包含 mov r0, r1
和 mov r1, r0
的包交换,具体取决于 ISA 是否允许同一包中的指令读取和写入相同的寄存器。
但 x86 不是这样的:超标量 out-of-order 执行必须始终保持 运行 指令在程序顺序中一次一条的错觉。 OoO exec 的基本规则是:不要破坏 single-threaded 代码。
任何违反此规定的行为只能通过 checking for hazards 或推测性地在检测到错误后回滚来完成。
脚注 2:(接脚注 1)
您可以获取/解码/发出两条 back-to-back inc eax
指令,但它们不能在同一周期内执行,因为寄存器重命名 + OoO 调度程序必须检测到第二条读取第一个的输出。