【问题标题】:How to ensure that one instruction is finished before a second instruction is started on a pipelined CPU?如何确保在流水线 CPU 上启动第二条指令之前完成一条指令?
【发布时间】:2017-12-21 14:53:14
【问题描述】:

假设有两条顺序指令,像这样:

instruction A
instruction B

由于 CPU 流水线,B 将在 A 完成之前启动。

是否存在确保 B 在 A 完成后启动的机制?

更新:
抱歉,我没有准确描述问题。我的意思是,这两条指令具有应用程序级别的排序依赖性,但没有危险。例如,在事务系统中,第一条指令将日志刷新到持久存储,第二条指令通知客户端有关事务提交。所以我们不能执行第二条指令,直到第一个完成。如何提供这个执行顺序?

【问题讨论】:

  • 你的意思是管道停顿?这与 C(或汇编)有什么关系?
  • 有许多障碍可以在各种指令集架构中实现,这个问题写得不够好,不知道要解决哪个问题。
  • 您要解决的实际问题是什么?对于 CPU 设计人员来说,无缘无故地添加指令以减慢速度是非常愚蠢的。各种 CPU 上都有指令,但它们用于解决特定的排序问题(如内存、陷阱等),不过我还没有看到为了它而停止流水线的特定指令。
  • CPU 内置了很多逻辑,以避免在 B 依赖于 A 时启动 B。如果您有特定情况(以某种方式)B 依赖于 A,但 B 在 A 完成之前运行,提供详细信息。
  • 我认为这个问题不值得这么多反对。尽管单独的流水线并不意味着“B 在 A 完成之前开始”(因为 CPU 需要多个执行单元才能实现超标量并最终实现 OoO),但序列化指令是编程的一个重要方面。

标签: assembly cpu cpu-architecture pipelining


【解决方案1】:

由于 CPU 流水线,B 将在 A 完成之前启动。

所以?为什么会出现这个问题?

在基本pipelined architecture 中,指令 A 将在第一个周期开始执行,然后指令 B 将在下一个周期开始执行。

basic 5-stage RISC pipeline 为例,如下所示:

Clock Cycle   |    1    |    2     |     3     |      4      |      5      |      6      |
--------------|---------------------------------------------------------------------------
Instruction A |  Fetch  |  Decode  |  Execute  | Mem. Access |  Writeback  |
Instruction B |         |  Fetch   |  Decode   |   Execute   | Mem. Access |  Writeback  |

处理器将在第一个时钟周期开始获取指令 A。在第二个时钟周期,它将开始解码指令 A,同时它同时获取指令 B。依此类推,沿着流水线向下。

之所以如此有效,是因为指令 fetching 单元是与指令 解码 单元完全分开的硬件(尽管两者都可以在相同的硅板),因此保持这些单元中的每一个同时被占用是有意义的。这是实现instruction-level parallelism (ILP)的一种机制。

最终,您可以看到指令 A 将在第 5 周期完成,而指令 B 直到第 6 周期才能完成。不过,这比指令 A 在第 5 周期完成而指令 B 无法完成要好得多 开始 直到第 6 周期,推迟到第 11 周期完成。

处理器内部的逻辑处理指令依赖关系,因此如果指令 B 以某种方式依赖于指令 A 的结果,处理器的解码器将能够检测到这一点并停止指令 B 的执行,直到它的数据是可用的(即,直到指令 A 在流水线中走得足够远以至于它的结果已经准备好)。这一切都为您无缝处理,但它确实会引入性能成本 (pipeline bubbles),因此您希望尽可能避免它。这意味着编写代码,使具有依赖关系的指令尽可能分散,并在其间穿插独立指令。

是否存在确保 B 在 A 完成后启动的机制?

是的,此类机制通常存在,但您通常不想使用它们,因为它们会破坏管道的全部优势,从而减慢执行速度。

这些机制被称为序列化指令(或有时称为“屏障”),因为它们建立了一个屏障,导致执行在特定点被序列化。

例如,在 x86 架构上,CPUID 指令是一个序列化指令(实际上是one of several)。所以你可以这样做:

Instruction A
CPUID
Instruction B

这将确保指令 B 在之后指令 A 完成执行之前不会开始。

来自英特尔架构手册:

CPUID 可以在任何特权级别执行,以序列化指令执行。序列化指令执行保证了对前一条指令的标志、寄存器和内存的任何修改都在下一条指令被获取和执行之前完成。

另请参阅:IA-32 Intel Architecture Software Developer's Manual, Volume 3 AP-485 第 7 章中的“序列化指令”,英特尔处理器标识和 CPUID 指令。

从技术上讲,这并不能保证指令 B 不会启动流水线。例如,处理器可能会在完成执行指令 A 之前解码和获取指令 B。但是,从程序员的角度来看(即,可观察的行为),就好像指令 B 仅在指令 A 已完成。

【讨论】:

  • 非常感谢您的帮助。
【解决方案2】:

有几种类型的序列化,仅用于指令,您需要序列化/记分板指令,这可以防止年轻的在前一个提交之前进入 OOO 机器。 CPUID 会这样做,但很重。其他一些说明也可以这样做(见下文)。

然后,有一些面向内存的机制来确保加载或存储缓冲区被耗尽,主要用于内存排序目的。 LFENCESFENCE 分别保证,而 MFENCE 两者都做。需要注意的是,这与指令序列化有些正交,例如英特尔的开发人员手册指出:

MFENCE 指令是针对所有加载和存储进行排序的 指令、其他 MFENCE 指令、任何 LFENCE 和 SFENCE 指令和任何序列化指令(例如 CPUID 操作说明)。 MFENCE 不序列化指令流

可能有组合,例如我认为锁定操作(例如锁定 inc)保证指令和内存序列化(第一个是由于内存排序,后者是由于原子性)。另请参阅此处第 3A 卷中的第 8 章 - https://software.intel.com/sites/default/files/managed/39/c5/325462-sdm-vol-1-2abcd-3abcd.pdf

最后,您还需要一种确保将数据写入内存(或在某些情况下为持久存储)的方法。存储缓冲区耗尽仍然意味着数据可以驻留在本地缓存中。在普通的 WB 内存中,这就足够了,因为任何其他观察者都必须窥探它并获取更新,但在某些情况下,您希望确保数据不会因崩溃而丢失。为此,您可以使用 CLFLUSH/CLFLUSHOPT 或 PCOMMIT ( deprecated on some systems) / CLWB

再一次 - 以上所有内容都有不同的含义,具体取决于您的需要。

【讨论】:

  • 小心PAUSE。首先,它在 Pentium 4 之前的 CPU 上没有任何意义,但仍然是流水线的。其次,它设计是为了向处理器提示代码序列是一个自旋等待循环(并不是说这真的是很好的编程实践),因此可以避免内存顺序冲突。它不是真正的序列化指令。将其视为让步处理器的更好方法,它只让步给其他线程,因此它只在单个超线程(SMT)CPU 的上下文中生成。 “重”自带连载版图。
  • @CodyGray,我试图表明指令排序与内存排序不同。另外 - pause 不仅让给另一个线程,否则在单线程模式下使用 in 毫无意义(您仍然会急于执行处理器可以容纳的尽可能多的自旋循环迭代)。但我同意 pause 的含义不一致(虽然,真的,谁在 P4 之前运行?),这不是一个好主意。删除它。
  • 由于超线程,您在单线程模式下使用PAUSE。否则没有意义,这就是为什么它在 P4 之前不存在。至于谁在P4之前运行任何东西,我不知道。我猜“现实世界”中没有人,但我们仍然会在这里收到很多关于 16 位 DOS 编程的问题。
猜你喜欢
  • 2020-08-29
  • 2013-09-08
  • 2019-03-01
  • 1970-01-01
  • 1970-01-01
  • 2017-06-02
  • 2017-04-15
  • 1970-01-01
  • 2015-12-17
相关资源
最近更新 更多