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

时间:2016-02-19 23:21:09

标签: c++ multithreading x86-64 lock-free stdatomic

我有类似的东西:

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

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

您可以很容易地想象使用atomic with load(memory_order_acquire)和store(memory_order_release)的acquire_load和release_store的实现。但是现在如果release_store是用_mm_stream_si64实现的,这是一个非临时写入,而不是针对x64上的其他商店进行排序的?如何获得相同的语义?

我认为以下是最低要求:

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)

这是对的吗?我非常肯定这里绝对需要sfence。但是那个lfence怎么样?是否需要或者简单的编译器障碍对于x64是否足够?例如asm volatile(“”:::“memory”)。根据x86内存模型,负载不会与其他负载重新排序。所以根据我的理解,只要存在编译器障碍,acquire_load()必须在if语句中的任何加载之前发生。

1 个答案:

答案 0 :(得分:6)

我对这个答案中的一些事情可能是错的(来自知道这些东西的人的校对欢迎!)。它基于阅读文档和Jeff Preshing的博客,而不是最近的实际经验或测试。

Linus Torvalds强烈建议不要试图发明自己的锁定,因为它很容易弄错。在为Linux内核编写可移植代码时,更多的是一个问题,而不是只有x86的东西,所以我觉得足够勇敢尝试为x86解决问题

使用NT存储的常规方法是连续执行大量操作,例如作为memset或memcpy的一部分,然后是SFENCE,然后是正常的发布存储到共享标志变量:{ {1}}。

对同步变量使用done_flag.store(1, std::memory_order_release)存储会损害性能。您可能希望在它指向的movnti中使用NT存储,但是从缓存中逐出指针本身是有悖常理的。 (如果 Foo存储在高速缓存中以开头,则逐出该高速缓存行;请参阅vol1 ch 10.4.6.2 Caching of Temporal vs. Non-Temporal Data)。

NT商店的重点是与非时态数据一起使用,如果有的话,这些数据不会再次(通过任何线程)再次使用。控制对共享缓冲区的访问的锁,或者生产者/消费者用来将数据标记为读取的标志, 期望被其他核读取。

您的功能名称也不能真正反映您正在做的事情。

x86硬件针对正常(非NT)版本存储进行了极大的优化,因为每个普通商店都是一个发布商店。硬件必须擅长x86才能快速运行。

使用普通存储/加载只需要访问L3缓存而不是DRAM,以便在Intel CPU上的线程之间进行通信。英特尔的大型包含 L3缓存可作为缓存一致性流量的后盾。探测来自一个核心的未命中的L3标签将检测到另一个核心在Modified or Exclusive state中具有高速缓存行的事实。 NT存储将需要同步变量一直到DRAM并返回另一个核心才能看到它。

NT流媒体商店的内存订购

movnt商店可以与其他商店重新订购,但旧商店。

  

英特尔x86 manual vol3, chapter 8.2.2 (Memory Ordering in P6 and More Recent Processor Families)

     
      
  • 读取不会与其他读取重新排序。
  •   
  • 写入不会与较旧的读取重新排序。 (注意没有例外)。
  •   
  • 写入内存不会与其他写入重新排序,但以下情况除外:      
  •   
  • ...关于clflushopt和围栏说明的内容
  •   

更新还有一条说明(在 8.1.2.2软件控制总线锁定中)说:

  

不要使用WC内存类型实现信号量。不要对包含用于实现信号量的位置的缓存行执行非临时存储。

这可能只是一个表现建议;他们没有解释是否会导致正确性问题。请注意,NT存储不是缓存一致的(数据可以位于行填充缓冲区中,即使同一行的冲突数据存在于系统中的其他位置或内存中)。也许您可以安全地使用NT存储作为与常规负载同步的发布存储,但是会遇到像rep stos这样的原子RMW操作的问题。

Release semantics使用在程序顺序之前的任何读取或写入操作来防止对写入释放进行内存重新排序。

要阻止使用早期存储重新排序,我们需要lock add dword [mem], 1指令,即使对于NT存储也是a StoreStore barrier。 (并且也是某些编译时重新排序的障碍,但是我不确定它是否会阻止早期的负载越过屏障。)正常的商店不需要任何类型的屏障指令来释放-stores,因此在使用NT商店时只需要SFENCE

对于加载:WB (write-back, i.e. "normal")内存的x86内存模型已阻止LoadStore重新排序,即使对于弱排序的商店也是如此,因此我们的LoadStore barrier effect不需要SFENCE, NT存储之前只有一个LoadStore编译器屏障。 至少在gcc的实现中,即使对于非原子加载/存储,LFENCE也是编译器障碍,但std::atomic_signal_fence(std::memory_order_release)只是atomic_thread_fence加载/存储的障碍(包括atomic<>)。使用mo_relaxed仍允许编译器更自由地将加载/存储重新排序为非共享变量。 See this Q&A for more

atomic_thread_fence

这存储到原子变量(注意缺少解除引用// The function can't be called release_store unless it actually is one (i.e. 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); } )。你的函数存储到它指向的&gFoo,这是非常奇怪的; IDK的重点是什么。另请注意,compiles as valid C++11 code

在考虑发布商店的含义时,请将其视为在共享数据结构上释放锁定的商店。在您的情况下,当发布商店变得全局可见时,任何看到它的线程都应该能够安全地取消引用它。

要进行获取加载,只需告诉编译器您想要一个。

x86不需要任何屏障指令,但指定Foo而不是mo_acquire会为您提供必要的编译屏障。作为奖励,此功能是可移植的:您将在其他架构上获得任何和所有必要的障碍:

mo_relaxed

您没有说明将Foo* acquire_load() { return gFoo.load(std::memory_order_acquire); } 存储在弱有序的WC (uncacheable write-combining)内存中。可能很难安排将您的程序数据段映射到WC内存...... gFoo简单地指向 WC内存,在您映射一些WC视频RAM之后。但是如果你想从WC内存中获取负载,你可能需要gFoo。 IDK。提出另一个问题,因为这个答案主要假设您正在使用WB内存。

请注意,使用指针而不是标志会创建数据依赖关系。我认为你应该能够使用LFENCE,即使在弱排序的CPU(除了Alpha之外)上也不需要障碍。一旦编译器足够先进以确保它们不会破坏数据依赖性,他们实际上可以制作更好的代码(而不是将gFoo.load(std::memory_order_consume)推广到mo_consume。在使用{{1在生产代码中,尤其要注意,正确地测试它是不可能的,因为预期未来的编译器会提供比实际编译器更少的保证。

最初我认为我们确实需要LFENCE才能获得LoadStore屏障。 (&#34;写入不能通过早期的LFENCE,SFENCE和MFENCE指令&#34;。这反过来阻止它们在LFENCE之前读取(在之前变为全局可见)。

请注意,LFENCE + SFENCE仍然比完整的MFENCE弱,因为它不是StoreLoad屏障。 SFENCE自己的文档说它已经订购了。 LFENCE,但英特尔手册vol3中的x86内存模型表没有提到。如果SFENCE在LFENCE之后才能执行,那么mo_acquire / mo_consume实际上可能比sfence更慢,但lfence / mfence / lfence会给出释放语义而没有完整的障碍。请注意,在一些后续加载/存储之后,NT存储可能会变得全局可见,这与正常的强排序x86存储不同。)

相关:NT加载

在x86中,每个加载都具有获取语义,但来自WC内存的加载除外。 SSE4.1 sfence是唯一的非临时加载指令,当在普通(WriteBack)内存上使用时,它不是 弱序。所以它也是一个获取负载(当用在WB存储器上时)。

请注意,movnti只有商店表单,而MOVNTDQA只有加载表单。但显然英特尔不能只称他们为movntdqmovntdqa。他们都有16B或32B的对齐要求,所以不让storentdqa对我来说没有多大意义。我猜SSE1和SSE2已经引入了一些已经使用loadntdqa助记符(如a)的NT商店,但直到多年后才在SSE4.1中加载。 (第二代Core2:45nm Penryn)。

文档说 mov...并没有改变上使用的内存类型的排序语义。

  

......实施   如果存储器源是WB(写入,则还可以使用与该指令相关联的非时间提示)   返回)记忆类型。

     

处理器的非临时提示的实现不会覆盖有效的内存类型语义,但是   提示的实现取决于处理器。例如,处理器实现可以选择   忽略提示并将指令作为任何存储器类型的正常MOVDQA处理。

实际上,目前的英特尔主流CPU(Haswell,Skylake)似乎忽略了从WB内存加载PREFETCHNTA和MOVNTDQA的提示。有关详细信息,请参阅Do current x86 architectures support non-temporal loads (from "normal" memory)?Non-temporal loads and the hardware prefetcher, do they work together?

另外,如果 在WC内存上使用它(例如copying from video RAM, like in this Intel guide):

  

因为WC协议使用弱有序的内存一致性模型,MFENCE或锁定指令   如果多个处理器可能引用相同的WC,则应与MOVNTDQA指令一起使用   内存位置或为了使处理器的读取与系统中其他代理的写入同步。

但是,并没有说明 应该如何使用。而且我不确定为什么他们会说MFENCE而不是LFENCE阅读。也许他们正在讨论写入设备内存,从设备内存读取的情况,其中存储必须针对负载(StoreLoad屏障)进行排序,而不仅仅是相互之间(StoreStore屏障)。

我在Vol3中搜索了movntps,并没有获得任何点击(在整个pdf中)。 MOVNTDQA的3次点击:所有关于弱排序和内存类型的讨论仅涉及商店。请注意,movntdqa早在SSE4.1之前就已引入。据推测它对某些东西有用,但IDK是什么。对于负载排序,可能只有WC内存,但我还没有读到什么时候有用。

对于弱排序的加载,

movntdq似乎不仅仅是一个LoadLoad屏障:它也会命令其他指令。 (但不是商店的全球可见性,只是他们的本地执行)。

来自英特尔的insn ref手册:

  

具体来说,LFENCE不会在所有先前的指令在本地完成之后执行,并且之后没有指示   开始执行直到LFENCE完成。
  ...
  LFENCE之后的指令可以在LFENCE之前从内存中获取,但是直到它们才会执行   LFENCE完成了。

LFENCE的条目建议使用LFENCE来防止它在之前的说明之前执行,当rdtsc不可用时(并且排序保证较差:{ {1}}不会停止按照指令执行操作)。 (LFENCE;RDTSC是围绕RDTSCP)序列化指令流的常见建议。