我正在阅读有关内存障碍的内容,我可以总结的是它们会阻止编译器重新排序指令。
所以在用户空间内存让我说我有
b = 0;
main(){
a = 10;
b = 20;
c = add(a,b);
}
编译器是否可以重新排序此代码,以便在b = 20
被调用后发生c = add()
赋值。
为什么我们在这种情况下不使用障碍?我在这里错过了一些基本的东西。
虚拟内存是否免于重新订购?
进一步扩展问题:
在网络驱动程序中:
1742 /*
1743 * Writing to TxStatus triggers a DMA transfer of the data
1744 * copied to tp->tx_buf[entry] above. Use a memory barrier
1745 * to make sure that the device sees the updated data.
1746 */
1747 wmb();
1748 RTL_W32_F (TxStatus0 + (entry * sizeof (u32)),
1749 tp->tx_flag | max(len, (unsigned int)ETH_ZLEN));
1750
当他说设备看到更新的数据时...如何将这与使用障碍的多线程理论联系起来。
答案 0 :(得分:3)
用户模式代码中内存屏障的使用频率低于内核模式代码,因为用户模式代码倾向于使用更高级别的抽象(例如pthread同步操作)。
在分析可能的操作顺序时,需要考虑两件事:
在您的示例中,编译器无法在b=20
之后重新排序c=add(a,b)
,因为c=add(a,b)
操作使用b=20
的结果。但是,编译器可能会重新排序这些操作,以便其他线程在与c
关联的内存位置发生更改之前,查看与b
关联的内存位置。
这是否真的发生取决于硬件实现的内存一致性模型。
至于编译器何时可以进行重新排序,您可以想象添加另一个变量,如下所示:
b = 0;
main(){
a = 10;
b = 20;
d = 30;
c = add(a,b);
}
在这种情况下,编译器可以自由地将d=30
赋值移到c=add(a,b)
之后。
然而,这整个例子太简单了。程序没有做任何事情,编译器可以消除所有操作,不需要向内存写任何东西。
在多处理器环境中,多个线程可以看到内存操作以不同的顺序发生。 Intel Software Developer's Manual在第3卷第8.2.3节中有一些例子。我复制了下面的截图,其中显示了可以重新排序加载和存储的示例。 还有一个good blog post提供了有关此示例的更多详细信息。
答案 1 :(得分:2)
运行代码的线程将始终 这是,好像规则是大多数编译器优化的原因。
在单个线程中,无序CPU跟踪依赖关系,以便为线程提供所有指令按程序顺序执行的错觉。但是,全局可见(对其他核心上的线程)效果可能会被其他核心无序看到。
只有在通过共享内存与其他线程交互的代码中才需要内存障碍(作为锁定或自身的一部分)。
Compilers can similarly do any reordering / hoisting they want, as long as the results are the same。 C ++内存模型非常弱,因此即使面向x86 CPU,也可以进行编译时重新排序。 (但当然不能重新排序会在本地线程中产生不同的结果。)C11 <stdatomic.h>
和等效的C ++ 11 std::atomic
是告诉编译器有关您的任何排序要求的最佳方式。全球运营可见度。在x86上,这通常只会导致存储指令按源顺序排列,但默认memory_order_seq_cst
在每个存储上需要MFENCE
,以防止StoreLoad重新排序以实现完全顺序一致性。
在内核代码中,内存屏障也很常见,以确保内存映射I / O寄存器的存储按所需顺序发生。原因是相同的:在一系列存储和加载的内存中排序全局可见的效果。区别在于观察者是I / O设备,而不是另一个CPU上的线程。核心通过缓存一致性协议相互交互的事实是无关紧要的。
答案 2 :(得分:1)
编译器无法重新排序(运行时或cpu也不能),因此b=20
位于c=add()
之后,因为这会改变方法的语义,这是不允许的。
我会说,对于编译器(或运行时或cpu),你所描述的行为会使行为随机,这将是一件坏事。
这种重新排序限制仅适用于执行代码的线程。正如@GabrielSouthern指出的那样,如果a
,b
和c
都是全局变量,则无法保证商店的全局可见性。