prg*_*enz 14 c++ x86 multithreading intrinsics memory-barriers
我阅读了"英特尔架构的英特尔优化指南指南".
但是,我仍然不知道何时应该使用
_mm_sfence()
_mm_lfence()
_mm_mfence()
Run Code Online (Sandbox Code Playgroud)
任何人都可以解释在编写多线程代码时何时应该使用它们?
如果您使用的是 NT 商店,您可能想要_mm_sfence甚至_mm_mfence. 的用例_mm_lfence更加模糊。
如果没有,只需使用 C++11 std::atomic 并让编译器担心控制内存排序的 asm 细节。
x86 有一个强有序的内存模型,但 C++ 有一个非常弱的内存模型(对于 C 也是如此)。 对于获取/释放语义,您只需要防止编译时重新排序。请参阅 Jeff Preshing 的“编译时内存排序”文章。
_mm_lfence并且_mm_sfence确实具有必要的编译器屏障效果,但它们也会导致编译器发出无用lfence或sfenceasm 指令,使您的代码运行速度变慢。
当您不做任何让您想要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 内存带宽的更多信息,请参阅增强型 REP MOVSB for memcpy。对于写入绝对不会很快被重新读取的非常大的缓冲区(大于 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);
}
Run Code Online (Sandbox Code Playgroud)
然后消费者可以安全地进行,if(p->buf_ready.load(std::memory_order_acquire)) { foo = p->buf[0]; ... }而无需任何数据竞争的未定义行为。读者方也没有需要_mm_lfence; NT 存储的弱排序性质完全限于执行写入的核心。一旦它成为全球可见的,它就完全一致并按照正常规则进行排序。
其他用例包括排序clflushopt以控制存储到内存映射非易失性存储的数据的顺序。(例如,现在存在使用 Optane 内存的 NVDIMM 或带有电池后备 DRAM 的 DIMM。)
_mm_lfence几乎从不用作实际负载栅栏。当从 WC(写组合)内存区域加载时,加载只能弱排序,比如视频内存。即使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 沙箱的程序中担心这一点,并且担心受到来自内部的攻击沙箱。
这是我的理解,希望准确和简单到有意义:
(Itanium) IA64 体系结构允许以任何顺序执行内存读取和写入,因此从另一个处理器的角度来看,内存更改的顺序是不可预测的,除非您使用栅栏强制以合理的顺序完成写入。
从这里开始,我说的是 x86,x86 是强序的。
在 x86 上,英特尔不保证在另一个处理器上完成的存储将始终在该处理器上立即可见。有可能这个处理器推测性地执行加载(读取)的时间刚好足够早,以至于错过了另一个处理器的存储(写入)。它仅保证写入对其他处理器可见的顺序是程序顺序。它不保证其他处理器会立即看到任何更新,无论您做什么。
锁定的读/修改/写指令是完全顺序一致的。因此,通常您已经处理了由于锁定xchg或cmpxchg将全部同步而丢失其他处理器的内存操作的情况,您将立即获取相关的缓存行以获取所有权并自动更新它。如果另一个 CPU 正在与您的锁定操作竞争,那么您将赢得比赛并且另一个 CPU 将错过缓存并在您锁定操作后取回它,或者他们将赢得比赛,而您将错过缓存并获得更新他们的价值。
lfence停止指令发布,直到lfence完成之前的所有指令。mfence特别是等待所有先前的内存读取完全进入目标寄存器,并等待所有先前的写入变得全局可见,但不会像lfence将要那样停止所有进一步的指令。sfence仅对存储执行相同操作,刷新写入组合器,并确保sfence在允许 之后的任何存储sfence开始执行之前,所有存储在 之前都是全局可见的。
在 x86 上很少需要任何类型的栅栏,除非您使用写组合内存或非临时指令,否则它们不是必需的,如果您不是内核模式(驱动程序)开发人员,则很少这样做。通常,x86 保证所有存储都按程序顺序可见,但它不保证 WC(写入组合)内存或执行显式弱排序存储的“非临时”指令,例如movnti.
因此,总而言之,除非您使用了特殊的弱排序存储或正在访问 WC 内存类型,否则存储始终按程序顺序可见。使用诸如xchg、 或xadd、 或cmpxchg等锁定指令的算法将在没有围栏的情况下工作,因为锁定指令是顺序一致的。
警告:我不是这方面的专家。我自己还在努力学习这个。但由于这两天没有人回复,看来内存栅栏指令方面的专家并不多。所以这是我的理解......
英特尔是一个弱有序的内存系统。这意味着你的程序可能会执行
array[idx+1] = something
idx++
Run Code Online (Sandbox Code Playgroud)
但在更改array之前,对idx的更改可能是全局可见的(例如,对于在其他处理器上运行的线程/进程) 。将sfence放置在两个语句之间将确保写入发送到 FSB 的顺序。
与此同时,另一个处理器运行
newestthing = array[idx]
Run Code Online (Sandbox Code Playgroud)
可能已缓存数组的内存并具有陈旧的副本,但由于缓存未命中而获取更新的idx 。解决方案是预先使用lfence以确保负载同步。
| 归档时间: |
|
| 查看次数: |
6488 次 |
| 最近记录: |