assembly - 使先前的内存存储对后续的内存加载可见

标签 assembly x86 synchronization sse memory-fences

我想用循环中调用的_mm256_stream_si256()将数据存储在一个大数组中。
据我所知,然后需要一个内存屏障来使这些更改对其他线程可见。 _mm_sfence()的描述说

对所有存储到内存指令执行序列化操作
在此指令之前发布的。保证每个
以程序顺序在前面的存储指令是全局可见的
在任何按程序顺序在栅栏后面的存储指令之前。

但是,我的当前线程的最新存储是否也可以在后续加载指令中看到(在其他线程中)?还是我必须致电_mm_mfence()? (后者似乎很慢)

更新:我之前看到了这个问题:when should I use _mm_sfence _mm_lfence and _mm_mfence。那里的答案通常集中在何时使用围栏上。我的问题更具体,该问题的答案不太可能解决此问题(并且当前不这样做)。

UPDATE2:在注释/答案之后,让我们将“后续负载”定义为线程中的负载,该负载随后获取当前线程当前持有的锁。

最佳答案

但是,以后的加载指令也可以看到我最近的商店吗?

这句话毫无意义。加载是任何线程都能看到内存内容的唯一方法。不知道为什么要说“太”,因为没有别的了。 (非CPU系统设备的DMA读取除外。)

可以在全球范围内看到的商店的定义是,任何其他线程中的负载都会从中获取数据。 意味着该存储区已离开CPU的专用存储缓冲区,并且是包含所有CPU的数据高速缓存的一致性域的一部分。 (https://en.wikipedia.org/wiki/Cache_coherence)。

CPU始终尝试尽快将存储从其存储缓冲区提交到全局可见的缓存/内存状态。您可以使用障碍进行的所有操作,就是让此线程等到发生这种情况后再进行后续操作。 在带有流存储的多线程程序中,这肯定是必需的,而这似乎正是您真正要问的。但是我认为重要的是要理解,即使没有同步,NT存储也确实可以很快迅速地被其他线程看到。

x86上的互斥锁解锁有时是lock add,在这种情况下,这已经是NT存储的完整屏障。但是,如果不能排除使用简单存储的互斥量实现,则至少需要sfence

普通的x86商店具有release memory-ordering semantics(C++ 11 std::memory_order_release)。 MOVNT流存储的顺序宽松,但是互斥锁/自旋锁功能以及对C++ 11 std::atomic的编译器支持基本上忽略了它们。 对于多线程代码,您必须自己对其进行防护,以免破坏互斥锁/锁定库函数的同步行为,因为它们仅同步常规的x86强序加载和存储。

执行存储的线程中的加载仍然始终会看到最新存储的值,即使从movnt存储中也是如此。您从不需要在单线程程序中使用隔离。乱序执行和内存重新排序的基本原则是,它永远不会打破在单个线程中按程序顺序运行的幻想。编译时重排序的问题相同:由于对共享数据的并发读/写访问是C++未定义的行为,因此,除非您使用围墙来限制编译时重排序,否则编译器仅必须保留单线程行为。

MOVNT + SFENCE在生产者-消费者多线程或正常锁定(自旋锁的解锁只是释放存储)的情况下很有用。

生产者线程使用流存储写入大缓冲区,然后将“true”(或缓冲区的地址,或其他内容)存储到共享标志变量中。 (Jeff Preshing calls this a payload + guard variable)。

使用者线程正在对该同步变量进行旋转,并在看到其变为真后开始读取该缓冲区。

生产者必须在写入缓冲区之后但在写入标志之前使用sfence,以确保在标志之前,缓冲区中的所有存储都是全局可见的。 (但是请记住,NT存储区仍然始终在当前线程的本地可见。)

(使用锁定库功能,存储到的标志是锁。其他尝试获取该锁的线程正在使用Acquisition-loads。)

std::atomic <bool> buffer_ready;

producer() {
    for(...) {
        _mm256_stream_si256(buffer);
    }
    _mm_sfence();

    buffer_ready.store(true, std::memory_order_release);
}

组装会像
 vmovntdqa [buf], ymm0
 ...
 sfence
 mov  byte [buffer_ready], 1

如果没有sfence,则某些movnt存储区可能会延迟到标志存储区之后,这违反了常规非NT存储区的发布语义。

如果您知道正在运行的硬件,并且知道缓冲区总是很大,那么如果您知道使用者始终从前到后读取缓冲区(按写入的顺序),则可以跳过sfence。 ,所以到消费者线程到达缓冲区末尾时,缓冲区末尾的存储可能仍无法在运行生产者线程的CPU核心的存储缓冲区中进行。


(in comments)
所谓“后续”,是指以后发生。

除非通过使用将生产者线程与使用者同步的方法来限制何时可以执行这些负载,否则无法实现此目的。就像说的那样,您要使用sfence使NT存储在执行的瞬间全局可见,以便在sfence之后执行1个时钟周期的其他内核上加载将看到这些存储。 一个合理的“后续”定义将是“在下一个使用该线程当前持有的锁的线程中”。

栅栏也比sfence更强大:

x86上的任何原子读取-修改-写入操作都需要lock前缀,这是一个完整的内存屏障(例如mfence)。

因此,例如,如果您在流存储之后增加一个原子计数器,则也不需要sfence。不幸的是,在C++中std:atomic_mm_sfence()彼此不了解,并且允许编译器遵循as-if规则优化原子。因此,很难确定lock编码的RMW指令将恰好位于您在生成的asm中所需的位置。

(基本上,if a certain ordering is possible in the C++ abstract machine, the compiler can emit asm that makes it always happen that way,例如,将两个连续的增量折叠为一个+=2,这样,任何线程都无法观察到该计数器为奇数。)

不过,默认的mo_seq_cst会阻止很多编译时重新排序,并且当您仅以x86为目标时,将其用于读取-修改-写入操作并没有太大的弊端。不过,sfence相当便宜,因此在某些流存储和lock ed操作之间尝试避免它很不值得。

相关:pthreads v. SSE weak memory ordering。该问题的提问者认为,解锁锁将始终执行lock ed操作,从而使sfence变得多余。

C++编译器不会尝试在流存储之后为您插入sfence,即使存在顺序比std::atomic更强的relaxed操作。对于编译器而言,很难过于保守地可靠地实现这一权利(例如,在调用程序使用原子的情况下,在具有NT存储的每个函数的末尾添加sfence)。

英特尔内部函数早于C11 stdatomic和C++ 11 std::atomicstd::atomic的实现假装不存在弱排序的商店,因此您必须使用内在函数将它们自己隔离开。

这似乎是一个不错的设计选择,因为您只想在特殊情况下使用movnt存储,这是因为它们的高速缓存行为。您不希望编译器在不需要的地方插入sfence,或在movnti中使用std::memory_order_relaxed

关于assembly - 使先前的内存存储对后续的内存加载可见,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/44864033/

相关文章:

iphone - 当同步访问对象时,是什么导致对象 [0] 处出现 nil 对象?

c - 位 vector 和浮点 vector 的快速点积

linux - Source Insight 中的 ARM 汇编

assembly - 8086中段寄存器的值是多少?

postgresql - 插入后同步两个表

javascript - Backbone.js 使用 native websockets 同步

c - 使用 nop 程序集精确延迟 Arduino?

assembly - 有什么方法可以触发 RDTSC 的传统模式吗?

assembly - 汇编中如何求偶数之和?

c - 为什么在使用内部函数时生成的程序集会重新排序?