在x64上使用非临时存储获取/释放语义

我有这样的东西:

if (f = acquire_load() == ) { ... use Foo } 

和:

 auto f = new Foo(); release_store(f) 

你可以很容易地想象一下,使用带有load(memory_order_acquire)和store(memory_order_release)的primefaces的acquire_load和release_store的实现。 但是现在如果release_store是通过_mm_stream_si64实现的,这是一个非暂时写入,而不是相对于x64上的其他存储进行sorting的呢? 如何获得相同的语义?

我认为以下是最低要求:

 atomic<Foo*> gFoo; Foo* acquire_load() { return gFoo.load(memory_order_relaxed); } void release_store(Foo* f) { _mm_stream_si64(*(Foo**)&gFoo, f); } 

并如此使用它:

 // thread 1 if (f = acquire_load() == ) { _mm_lfence(); ... use Foo } 

和:

 // thread 2 auto f = new Foo(); _mm_sfence(); // ensures Foo is constructed by the time f is published to gFoo release_store(f) 

那是对的吗? 我很确定这里是绝对必要的。 但是这个问题呢? 是否需要或将一个简单的编译器障碍足够的X64? 例如asm volatile(“”:::“memory”)。 根据x86内存模型,负载不会与其他负载重新sorting。 所以就我的理解而言,只要存在编译器障碍,acquire_load()必须在if语句内的任何加载之前发生。

我可能在这个答案中有些东西是错误的(校对来自知道这个东西的人的欢迎!)。 这是基于阅读文档和Jeff Preshing的博客,而不是最近的经验或testing。

Linus Torvalds强烈build议不要试图发明自己的locking,因为很容易弄错。 当为Linux内核编写可移植代码而不是x86内核时,这是一个更为棘手的问题,所以我觉得有足够的勇气去尝试为x86解决问题。


使用NT商店的正常方式是连续执行一堆,例如作为memset或memcpy的一部分,然后是SFENCE ,然后是正常的发行版存储到共享标志variables: done_flag.store(1, std::memory_order_release)

使用movnti存储到同步variables会损害性能。 你可能想在它指向的Foo使用NT存储,但是从caching中清除指针本身是不正当的。 ( movnt存储驱逐高速caching行,如果它在高速caching中开始 ;请参阅第10.4.6.2节“时态数据与非时态数据的高速caching” )。

NT商店的全部重点是用于Non-Temporal数据,如果有的话,这些数据不会再被任何线程使用。 控制对共享缓冲区的访问的锁或生产者/消费者用来标记数据为已读的标志预计将被其他内核读取。

你的函数名也不能真正反映你在做什么。

x86硬件对于正常(而不是NT)发布存储进行了极为优化,因为每个普通存储都是发布存储。 硬件必须善于使x86运行得更快。

使用正常的存储/加载只需要访问L3高速caching,而不是DRAM,以便在Intel CPU上的线程之间进行通信。 英特尔大容量的三级caching可用作caching一致性stream量的后盾。 从一个内核探测未命中的L3标签将检测到另一个内核具有处于修改或独占状态的高速caching行的事实。 NT商店将需要同步variables一路走出DRAM,并回到另一个核心看到它。


为NTstream媒体商店订购内存

movnt商店可以与其他商店重新sorting,但不适用于较旧的读取。

英特尔的x86手册第3卷第8.2.2章(P6中的内存sorting和更新的处理器系列) :

  • 读取不会与其他读取重新sorting。
  • 写入不会被旧的读取重新sorting 。 (注意没有例外)。
  • 写入内存不会与其他写入重新sorting,但以下情况除外:
    • 使用非暂时移动指令(MOVNTI,MOVNTQ,MOVNTDQ,MOVNTPS和MOVNTPD)执行的stream式存储(写入); 和
    • string操作(见第8.2.4.1节)。 (注意:从我读到的文档中, 快速string和ERMSB操作符在开始/结束时仍隐含有StoreStore的屏障,只有一个rep movsrep stos rep movs的商店之间存在潜在的重新sorting)。
  • …关于clflushopt和围栏说明的东西

更新:还有一个注释(在8.1.2.2软件控制总线locking ),它说:

不要使用WC内存types实现信号量。 不要对包含用于实现信号量的位置的高速caching行执行非临时存储。

这可能只是一个performancebuild议; 他们不解释是否会导致正确性问题。 请注意,虽然NT数据库不是caching一致的(即使同一行的冲突数据存在于系统中的其他位置或内存中,数据也可以位于行填充缓冲区中)。 也许你可以安全地使用NT商店作为与常规加载同步的发布商店,但是会遇到像lock add dword [mem], 1这样的primefacesRMW操作的问题。


释放语义可以防止对写入释放进行内存重新sorting,任何读取或写入操作都将按照程序顺序进行。

要阻止对较早的商店进行重新sorting,我们需要一个SFENCE指令,即使对于NT商店也是一个StoreStore屏障 。 (也是某种编译时重新sorting的障碍,但是我不确定它是否阻止了早期的加载越过障碍)。普通存储不需要任何types的障碍指令作为发布存储,所以使用NT商店时只需要使用SFENCE

对于加载:针对WB(回写,即“正常”)内存的x86内存模型已经可以防止LoadStore重新sorting,即使对于弱顺序的存储也是如此,所以我们不需要LFENCE作为其LoadStore屏障效果 ,只需要LoadStore编译器屏障在NT商店之前。 至less在gcc的实现中, std::atomic_signal_fence(std::memory_order_release)即使对于非primefaces的加载/存储也是编译器的障碍,但是atomic_thread_fence只是atomic<> loads / stores(包括mo_relaxed )的mo_relaxed 。 使用atomic_thread_fence仍允许编译器更加自由地将加载/存储重新sorting到非共享variables。 有关更多信息,请参阅此问答 。

 // The function can't be called release_store unless it actually is one (ie includes all necessary barriers) // Your original function should be called relaxed_store void NT_release_store(const Foo* f) { // _mm_lfence(); // make sure all reads from the locked region are already globally visible. Not needed: this is already guaranteed std::atomic_thread_fence(std::memory_order_release); // no insns emitted on x86 (since it assumes no NT stores), but still a compiler barrier for earlier atomic<> ops _mm_sfence(); // make sure all writes to the locked region are already globally visible, and don't reorder with the NT store _mm_stream_si64((long long int*)&gFoo, (int64_t)f); } 

这存储到primefacesvariables(注意缺乏解引用&gFoo )。 你的function存储到Foo它指向的,这是超级怪异的; IDK这是什么意思。 还要注意,它编译为有效的C ++ 11代码 。

在考虑发行版的含义时,考虑将其作为释放共享数据结构的锁的存储。 在你的情况下,当发布商店变得全局可见时,任何看到它的线程都应该能够安全地解引用它。


要做一个获取负载,只需告诉编译器你想要一个。

x86不需要任何屏障说明,但是指定mo_acquire而不是mo_relaxed会为您提供必要的编译器屏障。 作为奖励,这个function是可移植的:你将在其他架构上获得任何和所有必要的障碍:

 Foo* acquire_load() { return gFoo.load(std::memory_order_acquire); } 

你没有说什么关于存储gFoo在弱有序的WC(不可caching的写入组合)内存。 将程序的数据段映射到WC存储器可能是非常困难的…对于gFoo ,在你映射一些WCvideoRAM之后,简单地指向 WC存储器会容易gFoo 。 但是如果你想从WC内存获取负载,你可能需要LFENCE 。 IDK。 问另一个问题,因为这个答案主要是假设你正在使用WB内存。

请注意,使用指针而不是标志创build数据依赖关系。 我认为你应该可以使用gFoo.load(std::memory_order_consume) ,即使在弱sorting的CPU(不是Alpha)上也不需要屏障。 一旦编译器足够先进以确保它们不会破坏数据依赖性,它们实际上可以制作更好的代码(而不是将mo_consumemo_acquire 。在生产代码中使用mo_consume之前, mo_acquire阅读此内容,尤其要注意的是对它进行适当的testing是不可能的,因为未来的编译器在实践中要比现在的编译器做出更弱的保证。


最初我以为我们确实需要LFENCE来获得LoadStore的障碍。 (“写入不能通过早期的LFENCE,SFENCE和MFENCE指令”,这反过来阻止了它们在LFENCE之前的读取之前通过(在全局可见之前))。

请注意,LFENCE + SFENCE仍然比完整的MFENCE更弱,因为它不是StoreLoad的障碍。 SFENCE自己的文档说这是命令wrt。 LFENCE,但是英特尔手册vol3的x86内存模型的表格没有提到这一点。 如果SFENCE在LFENCE之后才能执行,那么sfence / lfence实际上可能会比mfence更慢,但是lfence / sfence / movnti会给出释放的语义而没有完整的障碍。 请注意,与正常的强sortingx86存储不同,NT商店可能会在一些以下加载/存储之后成为全局可见的。)


相关:NT负载

在x86中,除了WC内存的加载之外,每个加载都获取语义。 SSE4.1 MOVNTDQA是唯一的非暂时加载指令,在正常(回写)存储器上使用时不是弱有序的。 所以这也是一个获取负载(当在WB存储器上使用时)。

请注意, movntdq只有一个存储forms,而movntdqa只有一个加载forms。 但显然英特尔不能只把它们storentdqaloadntdqa 。 他们都有一个16B或32Balignment的要求,所以离开a对我来说没有什么意义。 我想SSE1和SSE2已经引入了一些已经使用mov...助记符(比如movntps )的NT商店,但是直到几年后才在SSE4.1中加载。 (第二代Core2:45nm Penryn)。

文档说MOVNTDQA不会改变它使用的内存types的sorting语义

…如果存储器源是WB(回写)存储器types,则实现也可以使用与该指令相关联的非暂时性提示。

处理器的非暂时性提示的实现不覆盖有效的存储器types语义 ,但是提示的实现是依赖于处理器的。 例如,处理器实现可以select忽略该提示,并将该指令作为用于任何存储器types的正常MOVDQA来处理。

实际上,目前的Intel主streamCPU(Haswell,Skylake)似乎忽略了从WB存储器中加载PREFETCHNTA和MOVNTDQA的提示 。 请参阅当前的x86体系结构是否支持非暂时性负载(来自“正常”内存)? ,还有非时间负载和硬件预取程序,它们一起工作吗? 更多细节。


此外,如果您在WC内存上使用它(例如从videoRAM复制,请参阅本英特尔指南 ):

由于WC协议使用弱顺序的内存一致性模型,因此如果多个处理器可能引用相同的WC存储单元或者为了使处理器的读取与其他代理的写入同步,则应该使用MFENCE或locking指令与MOVNTDQA指令结合使用在系统中。

但是,这并没有说明如何使用它。 我不确定他们为什么说读书而不是读书。 也许他们正在讨论写入设备内存,从设备读取内存的情况,在这种情况下,必须针对加载(StoreLoad障碍)对存储进行sorting,而不仅仅是相互(StoreStore障碍)。

我在第三卷searchmovntdqa ,并没有得到任何命中(在整个pdf中)。 对于movntdq命中3:所有关于弱sorting和内存types的讨论都只涉及商店。 请注意, LFENCE早在SSE4.1之前LFENCE已经推出了。 据推测这对于某些东西是有用的,但是IDK是什么。 对于负载sorting,可能只有WC内存,但我没有读到什么时候会有用。


LFENCE似乎不仅仅是一个负载较弱的负载的LoadLoad障碍:它也命令其他指令。 (不是商店的全球知名度,只是他们的本地执行)。

从英特尔的insn参考手册:

具体来说,LFENCE不会执行,直到所有先前的指令在本地完成,并且直到LFENCE完成后才会开始执行。

LFENCE之后的指令可以在LFENCE之前从内存中取出,但是直到LFENCE完成之后才会执行。

rdtsc的条目build议使用LFENCE;RDTSC来防止它在先前的指令之前执行,当RDTSCP不可用时(和较弱的顺序保证是好的: rdtscp不会停止遵循指令在其之前执行)。 ( CPUID是围绕rdtsc序列化指令stream的常见build议)。