【问题标题】:What happens to the CPU pipeline when the memory with the instructions is changed by another core?当带有指令的内存被另一个内核更改时,CPU 流水线会发生什么?
【发布时间】:2021-08-31 10:50:02
【问题描述】:

我试图了解 CPU 管道的“获取”阶段如何与内存交互。

假设我有这些说明:

4:  bb 01 00 00 00          mov    $1,%ebx
9:  bb 02 00 00 00          mov    $2,%ebx
e:  b3 03                   mov    $3,%bl

如果 CPU1 将 00 48 c7 c3 04 00 00 00 写入内存地址 8(即 64 位对齐)而 CPU2 正在执行这些相同的指令会发生什么?指令流会自动从 2 条指令变为 1 条指令,如下所示:

4:  bb 01 00 00 00          mov    $1,%ebx
9:  48 c7 c3 04 00 00 00    mov    $4,%rbx

由于 CPU1 正在写入 CPU2 正在读取的同一内存,因此存在争用。 写入会导致 CPU2 管道在刷新其 L1 缓存时停止吗? 假设 CPU2 刚刚完成了对mov $2 的“获取”pĥase,为了重新获取更新的内存,会被丢弃吗?

另外还有将 2 条指令变为 1 条指令时的原子性问题。

我找到了这个quite old document 提到“指令获取单元在每个时钟周期从指令高速缓存存储器中获取一个 32 字节的高速缓存行” 我认为这可以解释为每条指令都从 L1 获取缓存行的新副本,即使它们共享相同的缓存行。 但我不知道这是否/如何适用于现代 CPU。

如果上述情况正确,则意味着在将mov $2 提取到管道后,下一次提取可能会在地址e 处获取更新值并尝试执行00 00 (add %al,(%rax)),这将可能会失败。

但如果mov $2 的提取将mov $3 带入“指令缓存”,会不会 认为下一次提取只会从该缓存中获取指令(并返回mov $3)而不重新查询 L1 是否有意义? 这将有效地使这 2 条指令的获取原子化,只要它们共享一个缓存行。

那是什么?基本上有太多的未知数,太多我只能推测,所以我非常感谢管道的 2 个获取阶段如何与它们访问的内存交互(变化)的逐个时钟周期细分。

【问题讨论】:

  • 这完全取决于实现。不同的处理器以不同的方式处理这种情况。
  • 对于修改自己的代码的核心,请参阅:Observing stale instruction fetching on x86 with self-modifying code - 这是不同的(并且更难),因为必须对商店的无序执行进行排序从程序顺序中较早与较晚指令的代码获取。即商店必须变得可见的时刻是固定的,不像另一个核心,它只是在它发生时发生。

标签: assembly x86 pipeline cpu-architecture hotpatching


【解决方案1】:

正如 Chris 所说,RFO(Read For Ownership)可以随时使 I-cache 行无效。

根据超标量 fetch-groups 的排列方式,缓存行可能在 9: 获取 5 字节 mov 之间但在 e: 获取下一条指令之前无效。

当 fetch 最终发生时(该核心再次获取缓存行的共享副本),RIP = e 并将获取 mov $4,%rbx 的最后 2 个字节。 交叉修改代码需要确保没有其他内核在中间在它想要写一个长指令的地方执行。

在这种情况下,您会得到00 00 add %al, (%rax)

还要注意,写入 CPU 需要确保修改是原子的,例如使用 8 字节存储(Intel P6 和更高版本的 CPU 保证在 1 个高速缓存行内的任何对齐处存储多达 8 个字节是原子的;AMD 没有),或lock cmpxchglock cmpxchg16b。否则,读者可能会看到部分更新的说明。您可以将指令提取视为执行原子 16 字节加载或类似的操作。


“指令获取单元在每个时钟周期从指令高速缓存中获取一个 32 字节的高速缓存行”我认为可以解释为每条指令从 L1 获取高速缓存行的新副本,

没有。

然后将该宽提取块解码为多个 x86 指令!宽取指的重点是一次拉入多条指令,而不是为每条指令单独重做。该文档似乎是关于 P6 (Pentium III),尽管 P6 一次只执行 16 个字节的实际提取,进入一个 32 字节宽的缓冲区,让 CPU 占用一个 16 字节的窗口。

P6 是 3 宽的超标量,每个时钟周期可以解码最多 16 字节的机器代码,最多包含 3 条指令。 (但有一个预解码阶段首先找到指令长度......)

有关详细信息,请参阅 Agner Fog 的微架构指南 (https://agner.org/optimize/)(重点关注与提高软件性能相关的细节。)后来的微架构在预解码和解码之间添加了队列。请参阅 Agner Fog 的微架构指南和 https://realworldtech.com/merom/(核心 2)的这些部分。

当然,请参阅https://realworldtech.com/sandy-bridge,了解更现代的带有 uop 缓存的 x86。另外https://en.wikichip.org/wiki/amd/microarchitectures/zen_2#Core 表示最近的 AMD。

在阅读其中任何一篇之前,如果想获得良好的背景知识,请Modern Microprocessors: A 90-Minute Guide!


对于修改自己代码的核心,请参阅:Observing stale instruction fetching on x86 with self-modifying code - 这是不同的(并且更难)因为存储的乱序执行必须从程序中早期指令和后期指令的代码获取中排序命令。即商店必须变得可见的时刻是固定的,不像另一个核心,它只是在它发生时才发生。

【讨论】:

  • 啊,所以 fetch 阶段在高速缓存行上运行,并且与单个指令分离。与经典的 RISC 流水线不同。现在这一切都变得更有意义了。非常感谢您提供详细的答案和丰富的信息链接!
  • @Daniel:超标量 RISC 管道也可以进行更广泛的获取,并将其解码为 2 或 4 条指令。另请注意,Intel P6 实际上进行 32 字节宽的提取,只有 16 个。(即使当前的 Intel 一次也只能提取 16 个字节,所以它取决于 uop 缓存是否比这更快,例如在平均指令大小较大的代码区域中。)AMD 确实一次获取 32 个字节,IIRC,但他们后来采用了 uop 缓存。此外,现代 x86 具有 64 字节宽的高速缓存行。所以不要认为它是“整行”提取,只是“宽提取”,然后解码那个块或者直到一个分支。
【解决方案2】:

它因实现而异,但通常由多处理器的cache coherency protocol 管理。简而言之,当 CPU1 写入内存位置时,该位置将在系统中的所有其他缓存中失效。因此,该写入将使 CPU2 的指令缓存中的行以及 CPU2 的 uop 缓存中的任何(部分)解码指令无效(如果它有这样的东西)。因此,当 CPU2 去获取/执行下一条指令时,所有这些缓存都会丢失,并且在重新获取内容时会停止。根据缓存一致性协议,这可能涉及等待写入到达内存,或者可能直接从 CPU1 的 dcache 获取修改后的数据,或者事情可能通过一些共享缓存进行。

【讨论】:

  • 确实如此。但与Observing stale instruction fetching on x86 with self-modifying code 不同,它不必 必须使管道中已获取的指令无效(无管道核弹)。 I-fetch 是按顺序发生的,因此是否看到它只是在此核心使其缓存行副本无效之前或之后的问题。请注意,x86 具有一致的 I-cache,但其他一些 ISA 没有。至少在进行存储的核心上,需要使 I 缓存失效(并且可能将 D 缓存写回共享的外部级别),以便 fetch 可以看到它。
  • Re:缓存到缓存传输:更常见的机制是写回两个内核共享的缓存级别。这是现代 Intel / AMD CPU 上的 L3。缓存到缓存的传输也是一回事,例如Zen 上的 CCX 之间,或多核系统上的套接字之间(在这两种情况下,在 L3 缓存之间)。现代多核 CPU 肯定会避免将内核之间共享的数据写回 DRAM;内核间延迟对于往返 DRAM 来说太重要了。不过,理论上这在低性能设计中是可行的。
猜你喜欢
  • 2012-02-12
  • 2011-09-03
  • 2015-06-30
  • 2012-09-06
  • 2020-05-21
  • 1970-01-01
  • 2015-09-14
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多