问题描述
正如问题所述,我对内存屏障和仅编译器栅栏之间的区别感到困惑。
它们是一样的吗?如果不是,它们之间有什么区别?
解决方法
内存屏障在硬件中实现,并阻止 CPU 本身重新排序指令。
然而,仅编译器的栅栏会阻止编译器的优化器重新排序指令,但 CPU 仍然可以重新排序它们。
,作为一个具体的例子,考虑以下代码:
int x = 0,y = 0;
void foo() {
x = 10;
y = 20;
}
就目前而言,在没有任何障碍或围栏的情况下,编译器可以对两个存储重新排序并发出汇编(伪)代码,如
STORE [y],20
STORE [x],10
如果在 x = 10;
和 y = 20;
之间插入仅编译器围栏,编译器将被禁止这样做,而必须发出
STORE [x],10
STORE [y],20
但是,假设我们有另一个观察者查看内存中 x
和 y
的值,例如内存映射的硬件设备,或将要执行的另一个线程
void observe() {
std::cout << x << ",";
std::cout << y << std::endl;
}
(为简单起见,假设来自 x
中的 y
和 observe()
的加载不会以任何方式重新排序,并且加载和存储到 int
恰好是在这个系统上是原子的。)根据其加载发生在 foo()
中的存储的时间,我们可以看到它可以打印出 0,0
或 10,20
.看起来 0,20
是不可能的,但实际上并非如此。
即使 foo
中的指令以该顺序存储 x
和 y
,但在某些没有严格 store ordering 的架构上,也不能保证这些存储将成为 可见 observe()
以相同的顺序。可能是由于 out-of-order execution,执行 foo()
的核心实际上在将存储到 y
之前执行了存储到 x
。 (例如,如果包含 y
的缓存行已经在 L1 缓存中,但 x
的缓存行不在;CPU 不妨继续执行存储到 y
而不是而不是在加载 x
的缓存行时可能会停顿数百个周期。)或者,存储可以保存在 store buffer 中,并可能以相反的顺序刷新到 L1 缓存。无论哪种方式,observe()
都可能打印出 0,20
。
为了确保所需的排序,必须告诉 CPU 这样做,通常是通过在两个存储之间执行显式的内存屏障指令。这将导致 CPU 等待直到 x
的存储可见(通过加载缓存线、排空存储缓冲区等),然后才使 y
的存储可见。因此,如果您要求编译器放入内存屏障,它会发出类似
STORE [x],10
BARRIER
STORE [y],20
在这种情况下,您可以放心,observe()
将打印 0,20
,但绝不会打印 0,20
。
(请注意,这里做了许多简化假设。如果尝试用实际的 C++ 编写它,您需要使用 std::atomic
类型和 observe()
中的一些类似屏障来确保其加载没有重新排序。)