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

时间:2017-07-01 18:14:52

标签: 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:在评论/答案之后,让我们定义"后续加载"作为线程中的负载,随后获取当前线程当前持有的锁。

2 个答案:

答案 0 :(得分:8)

  

但是我的近期商店是否也可以在后续加载指令中看到?

这句话没什么意义。加载是任何线程可以看到内存内容的唯一方法。不知道你为什么说"太",因为没有别的。 (除非由非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 ++未定义行为,编译器只需保留单线程行为,除非您使用fence来限制编译时重新排序。

MOVNT + SFENCE在生产者 - 消费者多线程或普通锁定的情况下非常有用,其中自旋锁的解锁只是一个发布商店。

生产者线程用流媒体商店写一个大缓冲区,然后存储" true" (或缓冲区的地址,或其他)进入共享标志变量。 (Jeff Preshing calls this a payload + guard variable)。

消费者线程在该同步变量上旋转,并在看到它变为真后开始读取缓冲区。

生成器必须在写入缓冲区之后使用sfence,但在写入标志之前,要确保缓冲区中的所有存储在标志之前是全局可见的。 (但请记住,NT商店仍然总是本地立即可见到当前线程。)

(使用锁定库函数,存储的标志是锁。尝试获取锁的其他线程正在使用acquire-loads。)

std::atomic <bool> buffer_ready;

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

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

asm就像是

 vmovntdqa [buf], ymm0
 ...
 sfence
 mov  byte [buffer_ready], 1

如果没有sfence,某些movnt存储可能会延迟到标记存储之后,从而违反了普通非NT存储的发布语义。

如果您知道自己正在运行的硬件,并且知道缓冲区始终大,那么如果您始终了解消费者,则可能会跳过sfence从前到后读取缓冲区(按照与写入时相同的顺序),因此缓冲区末端的存储可能无法在存储缓冲区中传输到缓冲区的核心位置。在消费者线程到达缓冲区末尾时,CPU运行生产者线程。

  

(in comments)   通过&#34;后续&#34;我的意思是晚些时候发生。

除非您通过使用使生产者线程与消费者同步的内容来限制何时可以执行这些加载,否则无法实现此目的。如上所述,您要求sfence使NT存储在其执行的瞬间全局可见,因此在sfence之后执行1个时钟周期的其他内核上的负载将看到存储。 &#34;后续&#34;的明确定义将是&#34;在下一个获取该线程当前持有的锁定的线程中#34;。

强于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版本的操作之间尝试避免这种做法可能不值得。

相关:pthreads v. SSE weak memory ordering。该问题的提问者认为解锁锁定总是进行lock ed操作,从而使sfence多余。

C ++编译器在流媒体商店之后不会尝试为您插入sfence,即使有std::atomic次操作的排序强于relaxed。对于编译器而言,如果没有非常保守的话(例如sfence在每个具有NT存储的函数的末尾,如果调用者使用原子的话),就很难可靠地做到这一点。

英特尔内在函数早于C11 stdatomic和C ++ 11 std::atomic。  std::atomic的实施假装弱有序的商店并不存在,所以你必须自己用内在函数来围绕它们。

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

答案 1 :(得分:-1)

  

但是我最近的当前线程的商店是否可见   后续的加载指令(在其他线程中)?或者我有   调用_mm_mfence()? (后者似乎很慢)

答案是否定的。您不能保证在一个线程中看到以前的存储而不在其他线程中进行任何同步尝试。这是为什么?

  1. 您的编译器可以重新排序说明
  2. 您的处理器可以重新排序说明(在某些平台上)
  3. 在C ++编译器中,需要发出顺序一致的代码,但仅用于单线程执行。请考虑以下代码:

    int x = 5;
    int y = 7;
    int z = x;
    

    在这个程序中,编译器可以选择在x = 5之后放置y = 7,但不会在以后因为它不一致。
    如果您考虑在其他线程中使用以下代码

    int a = y;
    int b = x;
    

    这里可以发生相同的指令重新排序,因为a和b彼此独立。运行这些线程的结果是什么?

    a    b
    7    5
    7    ? - whatever was stored in x before the assignment of 5
    ...
    

    即使我们在x = 5y = 7之间设置了内存屏障,我们也可以获得这个结果,因为在a = yb = x之间没有设置障碍,你永远不知道在哪个顺序他们会被阅读。

    这只是在Jeff Preshing的博文Memory Ordering at Compile Time

    中您可以阅读的内容的粗略介绍