乱序执行和内存屏障

11

我知道现代CPU可以乱序执行,但是它们总是按照顺序退役结果,就像维基百科所描述的那样。

"乱序处理器用其他准备好的指令填补这些时间段,然后在最后重新排序结果,使其看起来像正常处理的指令一样。"

现在,在使用多核平台时,需要使用内存栅栏,因为由于乱序执行,这里可能会打印出错误的x值。

Processor #1:
 while f == 0
  ;
 print x; // x might not be 42 here

Processor #2:
 x = 42;
 // Memory fence required here
 f = 1
现在我的问题是,由于乱序处理器(我想是多核处理器的情况下的核心)总是按顺序撤回结果,那么内存障碍的必要性是什么?多核处理器的核心是否只看到其他核心退休的结果,还是也可以看到正在进行中的结果?
我的意思是,在我上面给出的例子中,当第2个处理器最终撤回结果时,x的结果应该先于f,对吗?我知道在乱序执行过程中,它可能已经修改了f,但它肯定在x之前未被撤回,对吗?
现在,有了按顺序撤销结果和缓存一致性机制,为什么您需要在x86中使用内存障碍呢?

请注意,在正确的代码中,内存栅栏总是成对出现:当两个线程进行通信时,每个线程都必须执行一些内存访问排序(=栅栏)。通常,其中一个栅栏具有释放语义,另一个栅栏具有获取语义。在您的伪代码中,处理器#2应在分配之间执行写入栅栏(释放语义),而处理器#1应在循环和“打印”之间添加读取栅栏(获取语义)。在特定平台上,某些栅栏可能是不必要的,但任何源代码都应包含两个栅栏(可能编译为noops)。 - cmaster - reinstate monica
3个回答

16
这篇教程解释了以下问题:http://www.hpl.hp.com/techreports/Compaq-DEC/WRL-95-7.pdf 顺带一提,现代x86处理器中发生内存排序问题的原因是,尽管x86内存一致性模型提供了相当强的一致性,但需要显式屏障来处理写后读一致性。这是由于一种称为“存储缓冲区”的东西。
也就是说,x86是顺序一致的(易于推理),除了加载可能与先前的存储重排序之外。也就是说,如果处理器执行以下序列:
store x
load y

如果在处理器总线上,这可能被视为

load y
store x

这种行为的原因是前面提到的存储缓冲区,它是用于在写入系统总线之前进行写入的小缓冲区。另一方面,加载延迟对性能是一个关键问题,因此允许加载“跳过队列”。
请参阅http://download.intel.com/design/processor/manuals/253668.pdf中的第8.2节。

Janneb,你能否简单解释一下存储缓冲区以及它们在这个上下文中的重要性吗? - MetallicPriest
缓存一致性难道不是确保x86中有读写一致性吗? - MetallicPriest
@MetallicPriest:啊,我再想一想,在你的具体示例中似乎并不需要屏障。我编辑了帖子以反映这一点,并添加了关于x86内存模型中允许重新排序的说明。 - janneb
@janneb,他从维基百科关于内存屏障的文章中借鉴了这个例子。 - Tony The Lion
1
对于FWIW和OTOH,减去一个。 - insumity
显示剩余2条评论

8
内存栅栏确保在栅栏之前对变量的所有更改对所有其他核心可见,以便所有核心都具有数据的最新视图。
如果您不放置内存栅栏,则核心可能使用错误的数据,尤其是在多个核心将处理相同数据集的情况下。在这种情况下,您可以确保当CPU 0执行某些操作时,对数据集所做的所有更改现在对所有其他核心可见,然后其他核心可以使用最新信息进行工作。
一些架构(包括普遍存在的x86 / x64)提供了几个内存屏障指令,包括一种称为“完整屏障”的指令。完整屏障确保在屏障之前的所有加载和存储操作将在屏障之后发布任何加载和存储之前提交。
如果核心开始使用数据集上过时的数据,它如何才能获得正确的结果?即使最终结果被呈现为按正确顺序完成了所有操作,它也无法获得正确结果。
关键在于存储缓冲区,它位于高速缓存和CPU之间,并执行以下操作:
存储缓冲区对远程CPU不可见 存储缓冲区允许将写入存储器和/或缓存保存以优化互连访问
这意味着事物将被写入此缓冲区,然后在某个时刻将缓冲区写入高速缓存。因此,高速缓存可能包含不是最新的数据视图,因此另一个CPU通过高速缓存一致性也将没有最新数据。必须刷新存储缓冲区才能使最新数据可见,我认为这基本上是内存栅栏在硬件级别引起的情况。
编辑:
对于您用作示例的代码,维基百科说:内存屏障可以在处理器#2分配给f之前插入,以确保x的新值在更改f的值时或之前对其他处理器可见。

2
只是为了明确之前答案中隐含的内容,这是正确的,但与内存访问不同:
CPU可以乱序执行,但它们始终按顺序退役结果。
指令的退役与执行内存访问是分开的,内存访问可能在指令退役时完成。
每个核心都会表现出自己的内存访问发生在退役时,但其他核心可能在不同的时间看到这些访问。
(在x86和ARM上,我认为只有存储器可观测到这一点,但例如,Alpha可能从存储器中加载旧值。x86 SSE2具有比正常x86行为更弱的保证指令)。
PS.据我所知,被放弃的Sparc ROCK实际上可以乱序退役,它花费了功率和晶体管来确定这是无害的。它被放弃是因为功耗和晶体管计数...我不相信任何通用CPU已经带到市场上以乱序退役。

1
已经提出了理论建议,支持乱序退役以使得在不仅限于将一个普通ROB扩展到一个不切实际的1k条目时,能够隐藏内存延迟,特别是千指令处理器。Google找到了这个链接,其中包含一篇关于一些随机网站的论文:http://cgi.di.uoa.gr/~halatsis/Advanced_Comp_Arch/General_presentations/ACM_online-Seminars/Valero/kilo-Instruction.pdf。另外还有https://www.csl.cornell.edu/~martinez/doc/taco04.pdf。 - Peter Cordes
1
顺便说一句,单个核心看到自己的内存访问是按顺序发生的,但它们不必等待退役。存储转发使得加载可以访问最近存储的数据,而无需等待存储退役并在此之后提交到L1D缓存。(来源:http://blog.stuffedcow.net/2014/01/x86-memory-disambiguation/) - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接