我什么时候应该使用_mm_sfence _mm_lfence和_mm_mfence

时间:2010-12-27 09:35:20

标签: c++ multithreading x86 intrinsics memory-barriers

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

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

_mm_sfence()
_mm_lfence()
_mm_mfence()

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

4 个答案:

答案 0 :(得分:3)

这是我的理解,希望足够准确和简单:

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

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

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

锁定读取/修改/写入指令完全顺序一致。因此,通常您已经处理了其他处理器的内存操作,因为锁定的xchgcmpxchg会将其全部同步,您将立即获取所有权的相关缓存行并以原子方式更新它。如果另一个CPU正在与您的锁定操作竞争,您要么赢得比赛而另一个CPU将错过缓存并在锁定操作后将其恢复,否则他们将赢得比赛,您将错过缓存并获得更新来自他们的价值。

lfence停止指令问题,直到lfence之前的所有指令都完成为止。 mfence专门等待所有先前的内存读取完全进入目标寄存器,并等待所有先前的写入全局可见,但不会像lfence那样停止所有其他指令。 sfence仅针对商店执行相同操作,刷新写入组合器,并确保sfence之前的所有商店在允许sfence之后的任何商店开始执行之前全局可见。

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

因此,总而言之,除非您使用了特殊的弱排序存储或访问WC内存类型,否则存储始终以程序顺序显示。使用锁定指令(如xchgxaddcmpxchg等)的算法可以在没有围栏的情况下工作,因为锁定的指令是顺序一致的。

答案 1 :(得分:1)

内在调用您在调用时提及所有simply insert sfencelfencemfence指令。那么问题就变成了#34;那些围栏指令的目的是什么?#34;?

简短的回答是lfence完全无用*sfence几乎完全无用于x86中用户模式程序的内存排序目的。另一方面,mfence充当了完整的内存屏障,因此如果附近lock已经有一些前缀指令,您可能会在需要屏障的地方使用它。需要。

较长但仍然很简短的答案是......

lfence

lfence被记录为在lfence之后针对负载之后的负载进行排序,但是这种保证已经为正常负载提供,而没有任何围栏:也就是说,英特尔已经保证& #34;负载未与其他负载重新排序"。实际上,这将lfence作为无序执行障碍留在用户模式代码中的目的,可能对于仔细计时某些操作很有用。

SFENCE

sfence被记录为按照lfence对加载的相同方式订购商店之前和之后,但就像加载一样,商店订单在大多数情况下已经得到了英特尔的保证。它没有的主要有趣情况是所谓的非临时存储,例如movntdqmovntimaskmovq和一些其他指令。这些说明不适用于正常的内存排序规则,因此您可以在这些商店和要强制执行相对订单的任何其他商店之间放置sfencemfence也可以用于此目的,但sfence更快。

MFENCE

与其他两个不同,mfence实际上做了一些事情:它充当了完整的内存屏障,确保所有先前的加载和存储在任何后续任务之前都已完成 1 加载或存储开始执行。这个答案太短,无法完全解释内存屏障的概念,但一个例子是Dekker's algorithm,其中每个想要进入临界区的线程存储到一个位置,然后检查另一个线程是否已存储东西到它的位置。例如,在主题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 lock-prefixed指令具有相同的全屏障效果,而这些通常/始终是(?)比mfence便宜。

1 例如,已经满足了载荷并且商店将变得全局可见(尽管只要可见效果与订单相同,它就会以不同的方式实现;就像"那发生了。)

答案 2 :(得分:1)

如果您正在使用NT商店,则可能需要_mm_sfence或甚至_mm_mfence_mm_lfence的用例更加模糊。

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

x86具有强排序的内存模型,但C ++的内存模型非常弱(C相同)。 对于获取/发布语义,您只需要阻止编译时重新排序。请参阅Jeff Preshing的Memory Ordering At Compile Time文章。

_mm_lfence_mm_sfence确实具有必要的编译器屏障效果,但它们也会导致编译器发出无用的lfencesfence asm指令代码运行得慢。

当你没有做任何可能让你想要sfence的晦涩的东西时,有更好的选项来控制编译时重新排序。

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

如果您正在使用C ++ 11 std :: atomic,则可以执行shared_var.store(tmp, std::memory_order_release)。在任何早期的C赋值之后,即使是非原子变量,也可以保证全局可见

如果您正在推出自己的C11 / C ++ 11版本_mm_mfence

std::atomic 可能非常有用,因为实际mfence指令是获得顺序一致性的一种方法,即阻止以后加载读取值,直到前面的存储变为全局可见。见Jeff Preshing的Memory Reordering Caught in the Act

但请注意,mfence在当前硬件上似乎比使用锁定原子RMW操作要慢。例如xchg [mem], eax也是一个完整的障碍,但运行速度更快,并且可以存储。在Skylake上,实现mfence的方式可以防止在它之后执行甚至非内存指令。请参阅the bottom of this answer

在没有内联asm的C ++中,您对内存障碍的选择更加有限(How many memory barriers instructions does an x86 CPU have?)。 mfence并不可怕,而且正是gcc和clang目前用来做顺序一致性商店的。

严格地说,如果可能,只使用C ++ 11 std :: atomic或C11 stdatomic;它更易于使用,并且您可以获得很好的代码。或者在Linux内核中,已经有内联asm的包装函数用于必要的障碍。有时这只是一个编译器障碍,有时它也是一个asm指令,可以获得比默认值更强的运行时排序。 (例如,为了完全屏障)。

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

到目前为止,

_mm_sfence是在C ++中实际使用的最可能障碍

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

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

与正常商店不同,NT商店的排序较弱,因此您需要sfence ,如果您关心将数据发布到另一个主题。如果不是(你最终会从这个帖子中读到它们,然后你就没有了。或者,如果您在告诉另一个线程数据准备好之前进行系统调用,那么它也会序列化。

使用NT存储时,

sfence(或其他障碍)是必要的,以便释放/获取同步。 C ++ 11 std::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:Is LFENCE serializing on AMD processors?。否则lfence在Bulldozer系列上每个时钟运行4次,因此显然不会序列化。)

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

答案 3 :(得分:0)

警告:我不是这方面的专家。我还在努力学习这个。但由于过去两天没有人回复,看来记忆围栏指示专家似乎并不多。所以这是我的理解......

英特尔是weakly-ordered内存系统。这意味着您的程序可能会执行

array[idx+1] = something
idx++

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

同时,另一个处理器运行

newestthing = array[idx]

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

This articlethis article可能会提供更好的信息