内存屏障和仅编译器栅栏有什么区别

问题描述

正如问题所述,我对内存屏障和仅编译器栅栏之间的区别感到困惑。

它们是一样的吗?如果不是,它们之间有什么区别?

解决方法

内存屏障在硬件中实现,并阻止 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

但是,假设我们有另一个观察者查看内存中 xy 的值,例如内存映射的硬件设备,或将要执行的另一个线程

void observe() {
    std::cout << x << ",";
    std::cout << y << std::endl;
}

(为简单起见,假设来自 x 中的 yobserve() 的加载不会以任何方式重新排序,并且加载和存储到 int 恰好是在这个系统上是原子的。)根据其加载发生在 foo() 中的存储的时间,我们可以看到它可以打印出 0,010,20 .看起来 0,20 是不可能的,但实际上并非如此。

即使 foo 中的指令以该顺序存储 xy,但在某些没有严格 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() 中的一些类似屏障来确保其加载没有重新排序。)