我可能对这个答案中的某些事情有误(知道这些东西的人欢迎校对!)。它基于阅读文档和 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_consume
到mo_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 存储可能会在随后的一些加载/存储之后变得全局可见。)sfence
movnti
相关:NT 负载
在 x86 中,每个加载都具有获取语义,但来自 WC 内存的加载除外。SSE4.1MOVNTDQA
是唯一的非临时加载指令,在普通(WriteBack)内存上使用时它不是弱排序的。所以它也是一个获取负载(在 WB 内存上使用时)。
请注意,movntdq
只有一个存储表单,而movntdqa
只有一个加载表单。但显然英特尔不能只调用它们storentdqa
和loadntdqa
. 它们都具有 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
)。