假设有两个顺序指令,如下所示:
instruction A
instruction B
由于 CPU 流水线,B 将在 A 完成之前启动。
是否存在确保 B 在 A 完成后启动的机制?
更新:
很抱歉我没有准确描述问题。我的意思是,这两个指令具有应用程序级排序依赖关系,但没有危险。例如,在事务系统中,第一条指令是将日志刷新到持久存储,第二条指令是通知客户端有关事务提交的信息。因此,在第一个完成之前,我们无法执行第二条指令。如何提供此执行订单?
由于 CPU 流水线,B 将在 A 完成之前启动。
所以?为什么这是一个问题?
在基本的流水线架构中,指令 A 将在第一个周期开始执行,然后指令 B 将在下一个周期开始执行。
以基本的 5 阶段 RISC 流水线为例,它看起来像这样:
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。等等,顺着管道。
这工作得如此之好的原因是,指令提取单元是与指令解码单元完全独立的硬件(即使两者都可以在同一块硅片上实现),因此同时占用这些单元中的每一个是有意义的。这是实现指令级并行性 (ILP) 的一种机制。
最终,您可以看到指令 A 将在周期 5 完成,而指令 B 要到周期 6 才能完成。尽管如此,这比指令 A 在第 5 周期完成而指令 B 直到第 6 周期才能开始,将其完成推迟到第 11 周期要好得多。
处理器内部的逻辑处理指令依赖关系,因此如果指令 B 以某种方式依赖于指令 A 的结果,处理器的解码器将能够检测到这一点,并将停止指令 B 的执行,直到其数据可用(即,直到指令 A 在管道中走得足够远,其结果已准备就绪)。这一切都可以为您无缝处理,但它确实会带来性能成本(管道气泡),因此您希望尽可能避免它。这意味着编写代码,以便具有依赖项的指令尽可能彼此分散,并在两者之间穿插独立的指令。
是否存在确保 B 在 A 完成后启动的机制?
是的,此类机制通常存在,但您通常不想使用它们,因为它们会破坏管道的全部优势,从而减慢执行速度。
这些机制被称为序列化指令(有时称为"屏障"),因为它们竖立了一个障碍,导致在特定点序列化执行。
例如,在 x86 体系结构上,CPUID
指令是序列化指令(实际上是几个指令之一)。所以你可以做:
Instruction A
CPUID
Instruction B
这将确保指令 B 在指令 A 完成执行之前不会开始。
来自英特尔架构手册:
CPUID
可以在任何权限级别执行,以序列化指令执行。序列化指令执行保证在获取和执行下一条指令之前完成对前一条指令的标志、寄存器和内存的任何修改。另请参阅:IA-32 英特尔架构软件开发人员手册第 3 卷 AP-485、英特尔处理器标识和 CPUID 指令第 7 章中的"序列化指令"。
从技术上讲,这并不能保证指令 B 不会启动管道。例如,处理器可以在完成执行指令 A 之前解码并获取指令 B。但是,从程序员的角度来看(即可观察的行为),就好像指令B仅在指令A完成后才启动。
有几种类型的序列化,仅供说明使用,您需要序列化/记分板指令,以防止较年轻的指令在前一个提交之前进入 OOO 机器。CPUID
这样做,但非常沉重。其他一些说明也可以做到这一点(见下文)。
然后,有面向内存的机制来确保加载或存储缓冲区被耗尽,主要用于内存排序目的。LFENCE
和SFENCE
分别保证这一点,而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或PCOMBUT(在某些系统上已弃用)/CLWB
同样 - 以上所有内容都有不同的含义,具体取决于您的需要。