内存屏障处理的问题究竟是什么?

What exactly is the problem that memory barriers deal with?

我现在正在努力解决内存障碍问题。我一直在阅读和观看有关该主题的视频,我想确保自己理解正确,并提出一两个问题。

我从准确理解问题开始。我们以下面这个经典例子作为讨论的基础:假设我们有2个线程运行ning在2个不同的核心

这是伪代码!

我们从 int f = 0; int x = 0; 开始,然后 运行 这些线程:

# Thread 1

while(f == 0);

print(x)
# Thread 2 

x = 42;
f = 1;

当然,这个程序想要的结果是线程1会打印42。

注意:我将“编译时重新排序”排除在讨论之外,我只想关注 运行 时间发生的事情,因此请忽略编译器可能进行的各种优化。

好的,据我了解,这里的问题是所谓的“内存重新排序”:CPU 可以自由重新排序内存操作,只要最终结果是程序所期望的。在这种情况下,在线程 2 中,f = 1 可能会在 x = 42 之前执行。在这种情况下,线程1会打印0,这不是程序员想要的。

至此,维基百科指出了另一种可能发生的情况:

Similarly, thread #1's load operations may be executed out-of-order and it is possible for x to be read before f is checked

既然我们现在谈论的是“乱序执行”——让我们暂时忽略核心缓存。那么让我们来分析一下这里发生了什么。从线程 2 开始——编译后的指令看起来(在伪汇编中)类似于:

1 put 42 into register1
2 write register1 to memory location of x
3 put 1 into register 2
4 write register2 to memory location of f

好的,所以我知道 3-4 可能会在 1-2 之前执行。但我不明白线程 1 中的等价物:

假设线程 1 的指令类似于:

1 load f to register1
2 if f is 0 - jump to 1
3 load x to register2
4 print register2

这里到底有什么问题? 3可以在1-2之前吗?

让我们继续:到目前为止,我们讨论了乱序执行,这让我想到了我的主要困惑:

作者在this great post中这样描述问题:每个核心都有自己的缓存,核心对缓存进行内存操作,而不是对主内存。内存从核心特定高速缓存到主内存(或共享高速缓存)的移动以不可预测的时间和顺序发生。因此在我们的示例中——即使线程 2 将按顺序执行其指令——x=42 的写入将发生在 f=1 之前,但这只会写入 core2 的缓存。将这些值移动到共享内存的顺序可能相反,因此会出现问题。

所以我不明白 - 当我们谈论“内存重新排序”时 - 我们是在谈论乱序执行,还是在谈论跨缓存的数据移动?

在我看来,您错过了最重要的事情!

由于编译器没有看到 xf 的变化有任何副作用,编译器也可以优化所有这些。而且带有条件 f==0 的循环将导致“无”,因为编译器只看到您之前为 f=0 传播了一个常量,它可以假设 f==0 将始终为真并优化它走了。

对于所有这些,您必须告诉编译器将会发生一些在给定代码流中看不到的事情。这可能类似于调用某些 semaphore/mutex/... 或其他 IPC 功能或使用 atomic vars.

如果你编译你的代码,我假设你或多或少得到“什么都没有”,因为对于两个代码部分中的每一个都没有任何影响并且编译器没有看到变量是从两个线程上下文中使用的并且优化了所有一切都消失了。

如果我们按照以下示例实现代码,我们会看到它失败并在我的系统上打印 0

int main()
{
    int f = 0; 
    int x = 0; 

    std::thread s( [&f,&x](){ x=42; f=1; } ); 

    while( f==0);
    std::cout << x << std::endl;

    s.join();
}

如果我们将 int f = 0; 更改为 std::atomic<int> f = 0,我们将得到预期的结果。

when we talk about "memory reordering" - do we talk about Out-of-order execution, or are we talking about the movement of data across caches?

当线程以特定顺序观察值的变化时,从程序员的角度来看,无法区分这是否是由于 out-of-order 加载的执行,存储缓冲区相对于加载延迟存储并可能让他们乱序提交(不管执行顺序如何),或者(假设在 CPU 没有连贯缓存的情况下)缓存同步。

或者甚至在逻辑核心之间转发存储数据而不通过缓存,在它提交到缓存并变得对所有核心可见之前。 但很少有其他人。

Real CPUs have coherent caches; once a value commits to cache, it's visible to all cores; it can't happen until other copies are already invalidated, so this is not the mechanism for reading "stale" data. Memory reordering on real-world CPUs is ,连贯缓存的读写顺序可能与程序顺序不同。不同步后缓存不会 re-sync;它首先保持一致性。

无论机制如何,重要的影响是另一个观察与您相同的变量的线程 reading/writing,可以看到效果以不同于程序集的顺序发生 program-order。

您的两个邮件问题都有相同的答案(是!),但原因不同。

首先让我们看一下 pseudo-machine-code

的这一段

Let's say the instructions of thread 1 will be something like:

1 load f to register1
2 if f is 0 - jump to 1
3 load x to register2
4 print register2

What exactly may be out of order here? 3 can be before 1-2?

为了回答您的问题,这是一个回响的“是!”。由于 register1 的内容与 register2 的内容没有任何关系,因此 CPU 可以愉快地(并且正确地)预加载 register2,这样当1,2循环终于断了,可以马上转到4.

举一个实际的例子,register1 可能是一个 I/O 外围寄存器绑定到一个轮询的串行时钟,而 CPU 只是在等待时钟转换为低电平,这样它就可以 bit-bang 将下一个值放到数据输出线上。这样做可以节省宝贵的数据获取时间,更重要的是可以避免外围数据总线上的争用。

所以,是的,这种重新排序非常好并且被允许,即使关闭优化 发生在单线程、单核 CPU 上。在循环中断后确保 register2 确实被读取的唯一方法是插入一个屏障。

第二个问题是关于缓存一致性的。再一次,对内存障碍需求的回答是“是的!你需要它们”。缓存一致性是一个问题,因为现代 CPUs 不直接与系统内存对话,而是通过它们的缓存。只要您只处理一个 CPU 核心和一个缓存,一致性就不是问题,因为同一核心上的所有线程 运行 确实针对同一缓存工作。然而,当您拥有多个具有独立缓存的内核时,它们对系统内存内容的各自看法可能会有所不同,并且需要某种形式的内存一致性模型。通过显式插入内存屏障,或在硬件级别。