首页 > 解决方案 > C11 独立内存屏障 LoadLoad StoreStore LoadStore StoreLoad

问题描述

我想在原子操作和非原子操作之间使用独立的内存屏障(我认为这根本不重要)。我想我了解存储屏障和负载屏障的含义,以及 4 种可能的内存重新排序;LoadLoad, StoreStore, LoadStore, StoreLoad.

但是,我总是觉得获取/发布概念令人困惑。因为在阅读文档的时候,acquire 不只讲loads,还讲stores,而release 不只讲stores,还讲loads。另一方面,普通负载障碍仅为您提供负载保证,而普通商店障碍仅为您提供商店保证。

我的问题如下。在 C11/C++11 中,将独立atomic_thread_fence(memory_order_acquire)视为负载屏障(防止LoadLoad重新排序)和atomic_thread_fence(memory_order_release)作为存储屏障(防止StoreStore重新排序)是否安全?

如果以上是正确的,我可以用什么来防止LoadStoreStoreLoad重新排序?

当然,我对可移植性很感兴趣,我不在乎上面在特定平台上产生了什么。

标签: c++cmemory-barriersmemory-modelstdatomic

解决方案


不,在轻松加载后的获取障碍会变成获取负载(在某些 ISA 上与仅使用获取负载相比效率低下),因此它必须阻止 LoadStore 和 LoadLoad

请参阅 https://preshing.com/20120913/acquire-and-release-semantics/以获取一些非常有用的顺序图,这些图显示并且发布存储需要确保所有先前的加载和存储都是“可见的”,因此需要阻塞 StoreStore 和 LoadStore。(商店部分为第二的重新排序)。尤其是这张图:

在此处输入图像描述

还有https://preshing.com/20130922/acquire-and-release-fences/

https://preshing.com/20131125/acquire-and-release-fences-dont-work-the-way-youd-expect/ 解释了 acq 和 rel栅栏的 2 向性质与 1 向性质acq 或 rel操作,如加载或存储。显然,有些人对保证的内容有误解atomic_thread_fence(),认为它太弱了。

为了完整起见,请记住,这些排序规则必须由编译器强制执行,以防止编译时重新排序,而不仅仅是运行时。

考虑作用于 C++ 抽象机中的 C++ 加载/存储的障碍可能最有效,无论它是如何在 asm 中实现的。但是有一些像 PowerPC 这样的极端案例,其心智模型并不能涵盖所有内容(IRIW 重新排序,见下文)。

我确实建议尝试从获取和释放操作的角度进行思考,以确保其他操作对彼此的可见性,并且绝对不要编写仅使用宽松操作和单独障碍的代码。 这可能是安全的,但通常效率较低。


关于 ISO C/C++ 内存/线程间排序的所有内容都是根据从发布存储中查看值的获取负载来正式定义的,从而创建“同步”关系,而不是关于控制本地重新排序的栅栏。

std::atomic明确保证所有线程同时看到更改的一致共享内存状态的存在。在您使用的心智模型中,通过在读取/写入单个共享状态时进行本地重新排序,当一个线程使其存储在所有其他线程全局可见之前使其对其他一些线程可见时,可能会发生 IRIW 重新排序。(就像在某些 SMT PowerPC CPU 上实际发生的那样。)。

在实践中,所有 C/C++ 实现都跨内核运行线程,这些内核确实具有共享内存的缓存一致视图,因此在读取/写入一致共享内存方面的心理模型具有控制本地重新排序的障碍。但请记住,C++ 文档不会讨论重新排序,只是讨论是否首先保证任何顺序。


要深入了解 C++ 如何描述内存模型与如何描述真实架构的 asm 内存模型之间的区别,另请参阅如何在 C++11 中实现 StoreLoad 屏障?(包括我在那里的回答)。还有atomic_thread_fence(memory_order_seq_cst) 是否具有完整内存屏障的语义?相关的。

fence(seq_cst)包括 StoreLoad(如果该概念甚至适用于给定的 C++ 实现)。我认为根据局部障碍进行推理,然后将其转换为 C++有效,但请记住,它没有模拟 C++ 允许的 IRIW 重新排序的可能性,以及在某些 POWER 硬件上的现实生活中发生的可能性。

还要记住,这比 在某些 ISA(尤其是 ARMv8)上var.load(acquire)要高效得多。var.load(relaxed); fence(acquire);

例如Godbolt 上的这个例子,由 GCC8.2 为 ARMv8 编译-O2 -mcpu=cortex-a53

#include <atomic>
int bad_acquire_load(std::atomic<int> &var){
    int ret = var.load(std::memory_order_relaxed);
    std::atomic_thread_fence(std::memory_order_acquire);
    return ret;
}

bad_acquire_load(std::atomic<int>&):
        ldr     r0, [r0]          // plain load
        dmb     ish               // FULL BARRIER
        bx      lr
int normal_acquire_load(std::atomic<int> &var){
    int ret = var.load(std::memory_order_acquire);
    return ret;
}

normal_acquire_load(std::atomic<int>&):
        lda     r0, [r0]            // acquire load
        bx      lr

推荐阅读