18

我有类似的东西:

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

和:

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

您可以很容易地想象acquire_load 和release_store 的实现,它使用带有load(memory_order_acquire) 和store(memory_order_release) 的原子。但是现在如果 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。但是围栏呢?对于 x64,它是必需的还是简单的编译器屏障就足够了?例如 asm volatile("": : :"memory")。根据 x86 内存模型,负载不会与其他负载重新排序。因此,据我了解,只要存在编译器障碍,acquire_load() 必须在 if 语句内的任何加载之前发生。

4

1 回答 1

7

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

Linus Torvalds 强烈建议不要尝试发明自己的锁定,因为很容易出错。在为 Linux 内核编写可移植代码时,这更像是一个问题,而不是只针对 x86 的代码,所以我有足够的勇气尝试为 x86 解决问题。


使用 NT 存储的正常方法是连续执行一堆,例如作为 memset 或 memcpy 的一部分,然后是一个SFENCE,然后是一个普通的发布存储到一个共享标志变量:done_flag.store(1, std::memory_order_release)

对同步变量使用movnti存储会损害性能。您可能想在Foo它指向的地方使用 NT 存储,但是从缓存中逐出指针本身是不正当的。(movnt如果缓存行开始在缓存中,存储会驱逐缓存行;请参阅vol1 ch 10.4.6.2 Caching of Temporal vs. Non-Temporal Data)。

NT 存储的全部意义在于与非临时数据一起使用,如果有的话,很长一段时间内都不会再次使用(任何线程)。控制对共享缓冲区的访问的锁,或者生产者/消费者用来将数据标记为已读的标志,预计将被其他内核读取。

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

x86 硬件针对执行普通(不是 NT)发布存储进行了极大优化,因为每个普通存储都是发布存储。硬件必须擅长它才能使 x86 快速运行。

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


NT 流存储的内存排序

movnt商店可以与其他商店重新排序,但不能与较旧的读取一起重新排序。

Intel's x86 manual vol3, Chapter 8.2.2 (Memory Ordering in P6 and More Recent Processor Families)

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

更新:还有一条注释(在8.1.2.2 Software Controlled Bus Locking中)说:

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

这可能只是一个性能建议;他们没有解释它是否会导致正确性问题。但请注意,NT 存储不是高速缓存一致的(即使同一行的冲突数据存在于系统的其他地方或内存中,数据也可以位于行填充缓冲区中)。也许您可以安全地将 NT 存储用作与常规加载同步的发布存储,但会遇到与lock add dword [mem], 1.


释放语义防止写释放的内存重新排序,任何读或写操作在程序顺序之前。

为了阻止对早期存储的重新排序,我们需要一条SFENCE指令,即使对于 NT 存储,它也是一个 StoreStore 屏障。(并且也是某些编译时重新排序的障碍,但我不确定它是否会阻止早期加载越过障碍。)普通存储不需要任何类型的障碍指令作为释放存储,所以只有SFENCE在使用 NT 商店时才需要。

对于负载:WB(回写,即“正常”)内存的 x86 内存模型已经阻止了 LoadStore 重新排序,即使对于弱排序的存储也是如此,因此我们不需要LFENCE为其LoadStore 屏障效果,之前只需要一个 LoadStore 编译器屏障新台币商店。至少在 gcc 的实现中,std::atomic_signal_fence(std::memory_order_release)即使对于非原子加载/存储也是编译器障碍,但atomic_thread_fence只是atomic<>加载/存储(包括mo_relaxed)的障碍。使用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)。您的函数存储到Foo它指向的,这非常奇怪;IDK 那是什么意思。另请注意,它编译为有效的 C++11 代码

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


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

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

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

你没有说任何关于存储gFoo在弱排序的WC(不可缓存的写组合)内存中的事情。安排程序的数据段映射到 WC 内存可能真的很困难......在映射一些 WC 视频 RAM 或其他东西之后gFoo,简单地指向WC 内存会容易得多。但是如果你想从 WC 内存中获取加载,你可能确实需要LFENCE. 身份证。问另一个问题,因为这个答案主要假设您正在使用 WB 内存。

请注意,使用指针而不是标志会创建数据依赖关系。我认为您应该能够使用gFoo.load(std::memory_order_consume),即使在弱序 CPU(Alpha 除外)上也不需要障碍。一旦编译器足够先进以确保它们不会破坏数据依赖性,它们实际上可以编写更好的代码(而不是提升mo_consumemo_acquire. 在生产代码中使用之前阅读此内容mo_consume,特别是注意注意正确测试它是不可能的,因为未来的编译器预计会提供比当前编译器在实践中更弱的保证。


最初我认为我们确实需要 LFENCE 来获得 LoadStore 屏障。(“写入不能传递更早的 LFENCE、SFENCE 和 MFENCE 指令”。这反过来又会阻止它们传递(之前变得全局可见)在 LFENCE 之前的读取)。

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


相关:NT 负载

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

请注意,movntdq只有一个存储表单,而movntdqa只有一个加载表单。但显然英特尔不能只调用它们storentdqaloadntdqa. 它们都具有 16B 或 32B 对齐要求,因此a对我来说省略这些并没有多大意义。我猜 SSE1 和 SSE2 已经引入了一些已经使用mov...助记符的 NT 商店(如movntps),但直到多年后的 SSE4.1 才加载。(第二代 Core2:45nm Penryn)。

文档说MOVNTDQA不会改变它使用的内存类型的排序语义

...如果内存源是 WB(回写)内存类型,则实现也可以使用与该指令关联的非临时提示。

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

在实践中,当前的英特尔主流 CPU(Haswell、Skylake)似乎忽略了从 WB 内存加载 PREFETCHNTA 和 MOVNTDQA 的提示。请参阅当前的 x86 架构是否支持非临时负载(来自“正常”内存)?,以及非临时负载和硬件预取器,它们是否一起工作?更多细节。


此外,如果您在 WC 内存上使用它(例如从视频 RAM 复制,如本英特尔指南中所示):

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

不过,这并没有说明它应该如何使用。而且我不确定他们为什么说 MFENCE 而不是 LFENCE 来阅读。也许他们在谈论写入设备内存、从设备内存读取的情况,其中存储必须根据负载进行排序(StoreLoad 屏障),而不仅仅是彼此之间(StoreStore 屏障)。

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


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

来自英特尔的 insn 参考手册:

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

的条目rdtsc建议使用LFENCE;RDTSC以防止它在先前的指令之前执行,当RDTSCP它不可用时(并且较弱的排序保证是可以的:rdtscp不会停止遵循在它之前执行的指令)。(CPUID是序列化指令流的常见建议rdtsc)。

于 2016-02-23T07:33:45.830 回答