内存重新排序是否对单处理器上的其他线程可见?

时间:2013-01-06 12:26:29

标签: c++ multithreading memory-barriers

现代CPU架构通常会采用可能导致无序执行的性能优化。在单线程应用程序中,也可能发生内存重新排序,但程序员看不到,就像按程序顺序访问内存一样。对于SMP来说,内存障碍可以用来强制执行某种内存排序。

我不确定,是关于单处理器中的多线程。请考虑以下示例:当线程1运行时,f的商店可能会在商店之前发生到x。假设在写入f之后以及写入x之前发生上下文切换。现在线程2开始运行,它结束循环并打印0,这当然是不可取的。

// Both x, f are initialized w/ 0.
// Thread 1
x = 42;
f = 1;

// Thread 2
while (f == 0)
  ;
print x;

上述情况是否可行?或者是否保证在线程上下文切换期间提交物理内存?

根据此wiki

  

当程序在单CPU 计算机上运行时,硬件会执行   必要的簿记,以确保程序执行就像所有   内存操作按照指定的顺序执行   程序员(程序顺序),所以不需要内存障碍。

虽然它没有明确提到单处理器多线程应用程序,但它包含了这种情况。

我不确定它是否正确/完整。请注意,这可能高度依赖于硬件(弱/强内存模型)。因此,您可能希望在答案中包含您知道的硬件。感谢。

PS。设备I / O等不是我关心的问题。它是一个单核单处理器。

编辑:感谢Nitsan的提醒,我们假设这里没有编译器重新排序(只是硬件重新排序),并且线程2中的循环没有被优化掉......更多,魔鬼在细节中。 / p>

8 个答案:

答案 0 :(得分:17)

作为C ++问题,答案必须是程序包含数据争用,因此行为未定义。实际上,这意味着它可以打印42以外的东西。

这与底层硬件无关。正如已经指出的那样,循环可以被优化掉,编译器可以重新排序线程1中的赋值,这样即使在单处理器机器上也可以发生结果。

[我假设使用"单处理器"机器,你的意思是具有单核和硬件线程的处理器。]

您现在说,您希望假设编译器重新排序或循环消除不会发生。有了这个,我们离开了C ++的领域,并且真正询问相应的机器指令。如果你想消除编译器重新排序,我们也可以排除任何形式的SIMD指令,并且一次只考虑在单个存储器位置上运行的指令。

所以基本上thread1在store-to-x store-to-f的顺序中有两个存储指令,而thread2有test-f-and-loop-if-not-zero(这可能是多个指令,但涉及到load-from-f)然后加载来自x。

在我知道或可以合理想象的任何硬件架构上,线程2将打印42。

一个原因是,如果单个处理器处理的指令在它们之间不是顺序一致的,那么很难断言程序的效果。

唯一可能干扰的事件是中断(用于触发抢占上下文切换)。一个假设的机器在中断时存储其当前执行管道状态的整个状态并在从中断返回时恢复它可能产生不同的结果,但是这样的机器是不切实际的并且不存在afaik。这些操作会产生相当多的额外复杂性和/或需要额外的冗余缓冲区或寄存器,所有这些都是没有充分理由的 - 除了破坏你的程序。真正的处理器在中断时刷新或回滚当前流水线,这足以保证单个硬件线程上所有指令的顺序一致性。

没有内存模型问题需要担心。较弱的内存模型来自单独的缓冲区和缓存,它们将单独的硬件处理器与它们实际共享的主内存或第n级缓存分开。单个处理器没有类似的分区资源,没有充分理由将它们用于多个(纯软件)线程。如果不存在单独的处理资源(处理器/硬件线程)以保持这些资源繁忙,那么没有理由使架构复杂化并浪费资源以使处理器和/或存储器子系统知道诸如单独的线程上下文之类的东西。 。

答案 1 :(得分:4)

强大的内存排序以与程序中定义的完全相同的顺序执行内存访问指令,通常称为“程序排序”。

可以采用较弱的内存排序来允许处理器重新排序内存访问以获得更好的性能,它通常被称为“处理器排序”。

AFAIK,上面描述的场景在英特尔ia32架构中可能,其处理器排序使这种情况失效。相关规则是(intel ia-32软件开发手册Vol3A 8.2 Memory Ordering):

写入不会与其他写入重新排序,但流式存储,CLFLUSH和字符串操作除外。

为了说明规则,它给出了一个类似的例子:

内存位置x,y,初始化为0;

主题1:

mov [x] 1
mov [y] 1

主题2:

mov r1 [y]
mov r2 [x]

r1 == 1且r2 == 0不允许

在您的示例中,线程1 在存储x之前无法存储f。

@Eric回复你的评论。

快速字符串存储指令“stosd”,可能存储字符串内部其操作。在多处理器环境中,当处理器存储字符串“str”时,另一个处理器可能会在str [0]之前观察到str [1]被写入,而逻辑顺序被假定为在str [1]之前写入str [0]; < / p>

但这些说明不会与任何其他商店重新订购。并且必须有精确的异常处理。当在stosd中间发生异常时,实现可以选择延迟它,以便所有无序子存储(不一定意味着整个stosd指令)必须在上下文切换之前提交。

编辑以解决所提出的声明,好像这是一个C ++问题:

即使这在C ++的上下文中也被考虑,据我所知,标准的确认编译器应 NOT 重新排序线程1中x和f的赋值。

$ 14年9月1日 在每个值之前,每个值计算和与完整表达式相关的副作用都会排序 计算和副作用与下一个要评估的完整表达相关联。

答案 2 :(得分:2)

这不是一个真正的C或C ++问题,因为你明确假设没有加载/存储重新排序,这两种语言的编译器完全允许这样做。

为了论证而允许这个假设,请注意循环可能永远不会退出,除非你要么:

  • 让编译器有理由相信f可能会发生变化(例如,将其地址传递给可以修改它的某个非可内联函数)
  • 将其标记为易失性或
  • 使其成为显式原子类型并请求获取语义

在硬件方面,您担心在上下文切换期间“提交”物理内存不是问题。两个软件线程共享相同的内存硬件和缓存,因此无论核心之间 之间的一致性/一致性协议存在何种不一致的风险。

说两个商店都已发出,内存硬件决定重新订购它们。这究竟意味着什么?也许f的地址已经在缓存中,所以它可以立即写入,但x的存储被推迟到获取该缓存行。好吧,来自x的读取取决于相同的地址,因此:

  • 在获取发生之前不会发生加载,在这种情况下,理智的实现必须在排队加载之前发出排队的存储
  • 或者加载可以查看队列并获取x的值而无需等待写入

无论如何,考虑切换线程所需的内核抢占本身会发出内核调度程序状态一致性所需的任何加载/存储障碍,显然硬件重新排序不会成为问题。这种情况。


真正的问题(你试图避免)是你假设没有编译器重新排序:这是完全错误的。

答案 3 :(得分:2)

您只需要一个编译器围栏。来自内存障碍link)上的Linux内核文档:

  

SMP内存障碍减少到单处理器上的编译器障碍   编译系统,因为假设CPU看起来像是   自我一致,并将正确地命令重叠访问   尊重自己。

为了进行扩展,在硬件级别上同步的原因是:

  1. 单处理器系统上的所有线程共享相同的内存,因此SMP系统上不会出现缓存一致性问题(例如传播延迟),

  2. 如果由于抢先上下文切换而刷新管道,则CPU执行管道中的任何无序加载/存储指令都将被提交或回滚完整。 / p>

答案 4 :(得分:1)

这段代码可能永远不会完成(在线程2中),因为编译器可以决定将整个表达式提升出循环(这类似于使用不易失的isRunning标志)。 这就是说你需要担心这里有两种类型的重新排序:编译器和CPU,两者都可以随意移动商店。请参阅此处:http://preshing.com/20120515/memory-reordering-caught-in-the-act以获取示例。此时,您在上面描述的代码受编译器,编译器标志和特定体系结构的支配。引用的维基是误导性的,因为它可能表明内部重新排序不受cpu /编译器的支配,而事实并非如此。

答案 5 :(得分:1)

就x86而言,从执行代码的角度来看,无序存储在程序流方面是一致的。在这种情况下,“程序流程”只是处理器执行的指令流,而不是受限于“在线程中运行的程序”的指令。上下文切换等所需的所有指令都被视为此流程的一部分,因此跨线程保持一致性。

答案 6 :(得分:0)

上下文切换必须存储整个机器状态,以便在挂起的线程恢复执行之前可以恢复它。机器状态包括处理器寄存器但不包括处理器管道。

如果您假设没有编译器重新排序,这意味着必须在上下文切换(即中断)之前完成所有“即时”硬件指令,否则它们会丢失并且不会被存储上下文切换机制。这是硬件重新排序的独立性。

在您的示例中,即使处理器交换两个硬件指令“x = 42”和“f = 1”,指令指针也已经在第二个指令之后,因此必须在上下文切换开始之前完成两个指令。如果不是这样,由于管道和缓存的内容不是“上下文”的一部分,它们将丢失。

换句话说,如果在IP寄存器指向“f = 1”之后的指令时发生导致ctx切换的中断,则该点之前的所有指令必须已完成所有效果。

答案 7 :(得分:0)

从我的角度来看,处理器逐个获取指令。 在你的情况下,如果&#34; f = 1&#34;在&#34; x = 42&#34;之前被推测性地执行,这意味着这两个指令都已经在处理器的管道中。调度当前线程的唯一可能方法是中断。但处理器(至少在X86上)将在提供中断之前刷新管道的指令。 因此无需担心单处理器中的重新排序。