19

我阅读了“英特尔架构的英特尔优化指南指南”。

但是,我仍然不知道应该何时使用

_mm_sfence()
_mm_lfence()
_mm_mfence()

任何人都可以解释在编写多线程代码时何时应该使用这些?

4

4 回答 4

7

如果您使用的是 NT 商店,您可能想要_mm_sfence甚至_mm_mfence. 的用例_mm_lfence更加模糊。

如果没有,只需使用 C++11 std::atomic 并让编译器担心控制内存排序的 asm 细节。


x86 有一个强排序的内存模型,但 C++ 有一个非常弱的内存模型(对于 C 也是如此)。 对于获取/释放语义,您只需要防止编译时重新排序。请参阅 Jeff Preshing 的“编译时内存排序”一文。

_mm_lfence并且_mm_sfence确实具有必要的编译器屏障效果,但它们也会导致编译器发出无用lfencesfenceasm 指令,从而使您的代码运行速度变慢。

当你没有做任何让你想要的晦涩的事情时,有更好的选择来控制编译时重新排序sfence

例如,GNU C/C++asm("" ::: "memory")是一个编译器屏障(所有值都必须在内存中与抽象机匹配,因为"memory"clobber),但不会发出 asm 指令。

如果你使用 C++11 std::atomic,你可以简单地做shared_var.store(tmp, std::memory_order_release). 这保证在任何早期的 C 分配之后变得全局可见,即使是对非原子变量也是如此。

_mm_mfence如果您正在滚动自己的 C11 / C++11 版本,这可能std::atomic很有用,因为实际mfence指令是获得顺序一致性的一种方法,即阻止以后的加载读取值,直到前面的存储变得全局可见。请参阅 Jeff Preshing在法案中的记忆重新排序

但请注意,mfence在当前硬件上这似乎比使用锁定的原子 RMW 操作要慢。egxchg [mem], eax也是一个完整的屏障,但是运行得更快,并且做一个存储。在 Skylake 上,mfence实现的方式可以防止乱序执行,即使是在它之后的非内存指令。请参阅此答案的底部

但是,在没有内联汇编的 C++ 中,您对内存屏障的选择更加有限(x86 CPU 有多少内存屏障指令?)。 mfence并不可怕,它是 gcc 和 clang 目前用来进行顺序一致性存储的。

不过,如果可能,请认真使用 C++11 std::atomic 或 C11 stdatomic;它更易于使用,并且您可以在很多事情上获得相当不错的代码生成。或者在 Linux 内核中,已经有用于内联 asm 的包装函数,用于必要的屏障。有时这只是一个编译器障碍,有时它也是一个 asm 指令,以获得比默认值更强的运行时排序。(例如,对于一个完整的障碍)。


没有障碍会使您的商店更快地出现在其他线程中。他们所能做的就是延迟当前线程中的后续操作,直到更早的事情发生。CPU 已经尝试尽快将挂起的非推测性存储提交到 L1d 缓存。


_mm_sfence是迄今为止在 C++ 中实际手动使用的最有可能的障碍

的主要用例_mm_sfence()是在一些_mm_stream商店之后,在设置其他线程将检查的标志之前。

有关 NT 存储与常规存储以及 x86 内存带宽的更多信息,请参阅用于 memcpy 的增强型 REP MOVSB 。对于写入绝对不会很快重新读取的非常大的缓冲区(大于 L3 缓存大小),使用 NT 存储可能是个好主意。

NT 存储是弱排序的,与普通存储不同,因此sfence 如果您关心将数据发布到另一个线程,则需要。 如果不是(你最终会从这个线程中阅读它们),那么你不会。或者,如果您在告诉另一个线程数据准备好之前进行系统调用,那也是序列化。

sfence(或其他一些障碍)是在使用 NT 存储时为您提供释放/获取同步所必需的。 C++11std::atomic实现留给你来保护你的 NT 存储,以便原子发布存储可以​​高效。

#include <atomic>
#include <immintrin.h>

struct bigbuf {
    int buf[100000];
    std::atomic<unsigned> buf_ready;
};

void producer(bigbuf *p) {
  __m128i *buf = (__m128i*) (p->buf);

  for(...) {
     ...
     _mm_stream_si128(buf,   vec1);
     _mm_stream_si128(buf+1, vec2);
     _mm_stream_si128(buf+2, vec3);
     ...
  }

  _mm_sfence();    // All weakly-ordered memory shenanigans stay above this line
  // So we can safely use normal std::atomic release/acquire sync for buf
  p->buf_ready.store(1, std::memory_order_release);
}

然后,消费者可以安全地做if(p->buf_ready.load(std::memory_order_acquire)) { foo = p->buf[0]; ... }任何数据竞争未定义行为。读者方不需要_mm_lfence;NT 存储的弱排序特性完全局限于编写代码的核心。一旦它变得全局可见,它就会完全连贯并根据正常规则排序。

其他用例包括排序clflushopt以控制存储到内存映射非易失性存储的数据的顺序。(例如,现在存在使用 Optane 内存的 NVDIMM,或带有电池后备 DRAM 的 DIMM。)


_mm_lfence几乎从不用作实际的负载围栏。从 WC(写入组合)内存区域(如视频 RAM)加载时,加载只能是弱排序的。即使movntdqa( _mm_stream_load_si128) 在正常(WB = 回写)内存上仍然是强排序的,并且没有做任何事情来减少缓存污染。(prefetchnta可能,但很难调整,并且会使事情变得更糟。)

TL:DR:如果您不编写图形驱动程序或其他直接映射视频 RAM 的东西,则无需_mm_lfence订购负载。

lfence确实具有有趣的微体系结构效果,即在它退休之前阻止执行后面的指令。_rdtsc()例如,当早期的工作仍在微基准测试中未决时停止读取循环计数器。(始终适用于 Intel CPU,但仅适用于具有 MSR 设置的 AMD:LFENCE 是否在 AMD 处理器上序列化?否则lfence在 Bulldozer 系列上每个时钟运行 4 个,因此显然不序列化。)

由于您使用的是 C/C++ 的内在函数,因此编译器正在为您生成代码。您没有对 asm 的直接控制,但_mm_lfence如果您可以让编译器将其放在 asm 输出中的正确位置,您可能会使用诸如 Spectre 缓解之类的东西:在条件分支之后,在双数组访问之前. (如foo[bar[i]])。如果你为 Spectre 使用内核补丁,我认为内核会保护你的进程免受其他进程的攻击,所以你只需要在使用 JIT 沙箱的程序中担心这一点,并且担心会受到自身内部的攻击沙盒。

于 2018-06-10T03:27:00.223 回答
5

这是我的理解,希望足够准确和简单以至于有意义:

(Itanium) IA64 架构允许以任何顺序执行内存读取和写入,因此从另一个处理器的角度来看,内存更改的顺序是不可预测的,除非您使用栅栏来强制以合理的顺序完成写入。

从这里开始,我说的是 x86,x86 是强排序的。

在 x86 上,英特尔不保证在另一个处理器上完成的存储将始终在该处理器上立即可见。有可能这个处理器推测性地执行了加载(读取),刚好错过了其他处理器的存储(写入)。它只保证写入对其他处理器可见的顺序是程序顺序。它不保证其他处理器会立即看到任何更新,无论您做什么。

锁定的读/修改/写指令是完全顺序一致的。因此,通常您已经处理了丢失其他处理器的内存操作,因为锁定xchgcmpxchg将全部同步,您将立即获取相关的缓存行以获得所有权,并将自动更新它。如果另一个 CPU 与您的锁定操作竞争,您将赢得比赛,而另一个 CPU 将错过缓存并在您锁定操作后将其取回,或者他们将赢得比赛,您将错过缓存并获得更新他们的价值。

lfence停止指令发出,直到完成之前的所有指令lfencemfence特别是等待所有先前的内存读取完全进入目标寄存器,并等待所有先前的写入成为全局可见的,但不会停止所有进一步的指令lfencesfence仅对存储执行相同的操作,刷新写入组合器,并确保在允许后面的任何存储开始执行sfence之前,之前的所有存储都是全局可见的。sfence

x86 上很少需要任何类型的栅栏,除非您使用写组合内存或非临时指令,否则它们不是必需的,如果您不是内核模式(驱动程序)开发人员,您很少会这样做。通常,x86 保证所有存储在程序顺序中都是可见的,但它不保证 WC(写入组合)内存或执行显式弱排序存储的“非临时”指令,例如movnti.

因此,总而言之,除非您使用了特殊的弱排序存储或正在访问 WC 内存类型,否则存储始终按程序顺序可见。xchg使用, or xadd, or等​​锁定指令的算法cmpxchg可以在没有栅栏的情况下工作,因为锁定指令是顺序一致的。

于 2012-10-11T23:45:33.810 回答
3

您提到的所有内在调用都只是在调用它们时插入一个sfence, lfenceormfence指令。那么问题就变成了“这些围栏指令的目的是什么”?

简短的回答是,对于 x86 中的用户模式程序的内存排序目的,lfence它完全没用*并且几乎完全没用。sfence另一方面,mfence用作完整的内存屏障,因此如果附近没有一些lock以 - 为前缀的指令提供您需要的东西,您可以在需要屏障的地方使用它。

更长但仍然简短的答案是......

栅栏

lfence被记录为在之前的负载之前订购负载lfence,但是对于完全没有任何围栏的正常负载已经提供了这种保证:也就是说,英特尔已经保证“负载不会与其他负载一起重新排序”。实际上,这将lfence用户模式代码的目的作为无序执行障碍,可能对仔细计时某些操作很有用。

栅栏

sfence以与加载相同的方式记录前后顺序存储lfence,但就像加载一样,在大多数情况下,英特尔已经保证了存储顺序。它不存在的主要有趣案例是所谓的非临时存储,例如,movntdq和其他一些指令。这些指令不符合正常的内存排序规则,因此您可以在这些存储和您想要强制执行相对顺序的任何其他存储之间放置一个。也适用于此目的,但速度更快。movntimaskmovqsfencemfencesfence

围墙

与其他两个不同,mfence它实际上做了一些事情:它充当完整的内存屏障,确保所有先前的加载和存储都将在任何后续加载或存储开始执行之前完成1 。这个答案太短,无法完全解释内存屏障的概念,但一个例子是Dekker 算法,其中每个想要进入临界区的线程存储到一个位置,然后检查另一个线程是否存储了一些东西到它的地点。例如,在线程 1 上:

mov   DWORD [thread_1_wants_to_enter], 1  # store our flag
mov   eax,  [thread_2_wants_to_enter]     # check the other thread's flag
test  eax, eax
jnz   retry
; critical section

在这里,在 x86 上,您需要在存储(第一个mov)和加载(第二个mov)之间设置一个内存屏障,否则每个线程在读取对方的标志时可能会看到零,因为 x86 内存模型允许重新加载与较早的商店订购。mfence因此,您可以按如下方式插入屏障以恢复顺序一致性和算法的正确行为:

mov   DWORD [thread_1_wants_to_enter], 1  # store our flag
mfence
mov   eax,  [thread_2_wants_to_enter]     # check the other thread's flag
test  eax, eax
jnz   retry
; critical section

在实践中,您并没有看到mfence预期的那么多,因为 x86锁定前缀指令具有相同的全屏障效果,而且这些指令通常/总是(?)比mfence.


1例如,负载将得到满足,并且存储将变得全局可见(尽管只要对排序的可见效果“好像”发生了,它就会以不同的方式实现)。

于 2018-06-09T01:56:21.443 回答
1

警告:我不是这方面的专家。我自己还在努力学习这个。不过这两天没有人回复,看来内存栅栏指令方面的专家并不多。所以这是我的理解...

英特尔是一个弱有序的内存系统。这意味着您的程序可以执行

array[idx+1] = something
idx++

但是在更改为array之前,对idx的更改可能是全局可见的(例如,对于在其他处理器上运行的线程/进程)。在两个语句之间放置sfence将确保写入发送到 FSB 的顺序。

同时,另一个处理器运行

newestthing = array[idx]

可能已经缓存了数组的内存并且有一个陈旧的副本,但是由于缓存未命中而获得了更新的idx 。解决方案是预先使用lfence来确保负载同步。

这篇文章这篇文章可能会提供更好的信息

于 2010-12-29T14:04:25.603 回答