c++ - mov + mfence 在 NUMA 上安全吗?
问题描述
我看到 g++ 生成了一个简单mov
的 forx.load()
和mov
+ mfence
for x.store(y)
。考虑这个经典的例子:
#include<atomic>
#include<thread>
std::atomic<bool> x,y;
bool r1;
bool r2;
void go1(){
x.store(true);
}
void go2(){
y.store(true);
}
bool go3(){
bool a=x.load();
bool b=y.load();
r1 = a && !b;
}
bool go4(){
bool b=y.load();
bool a=x.load();
r2= b && !a;
}
int main() {
std::thread t1(go1);
std::thread t2(go2);
std::thread t3(go3);
std::thread t4(go4);
t1.join();
t2.join();
t3.join();
t4.join();
return r1*2 + r2;
}
其中根据https://godbolt.org/z/APS4ZY go1 和 go2 被翻译成
go1():
mov BYTE PTR x[rip], 1
mfence
ret
go2():
mov BYTE PTR y[rip], 1
mfence
ret
对于这个例子,我询问线程 t3 和 t4 是否有可能对 t1 和 t2 完成的写入“涓涓细流”到它们各自的内存视图的顺序存在分歧。特别考虑一个 NUMA 体系结构,其中 t3 恰好与 t1 “更接近”,而 t4 与 t2 “更接近”。t1 或 t2 的存储缓冲区是否会在到达之前“过早刷新” mfence
,然后 t3 或 t4 有机会比计划更早地观察到写入?
解决方案
是的,它是安全的。您不需要为 NUMA 安全代码启用特殊的编译器选项,因为 asm 不需要不同。
NUMA 甚至与此无关;多核单插槽 x86 系统已经可以在 x86 内存模型允许的范围内执行尽可能多的内存重新排序。(可能不那么频繁或时间窗更小。)
TLDR.1:您似乎误解了什么mfence
。它是运行它的核心的本地屏障(包括 StoreLoad,唯一的重新排序 x86 确实允许非 NT 加载/存储没有屏障)。这与此完全无关,即使 x86 是弱排序的: 我们正在查看每个来自不同内核的 1 个存储,因此对单个内核的操作进行排序。彼此无所谓。
(mfence
只是让该核心等待执行任何加载,直到其存储全局可见。当存储在mfence
等待它时提交时没有什么特别的事情发生。 内存屏障是否确保缓存一致性已经完成?。)
TL:DR.2:对不同线程中不同位置的两次原子写入是否总是以相同的顺序被其他线程看到?C++ 允许不同的线程在存储顺序上不同意放宽或释放存储(当然获取负载以排除 LoadLoad 重新排序),但不能使用seq_cst
.
在可能的架构上,编译器需要在 seq-cst 存储上设置额外的屏障来防止它。 在 x86 上这是不可能的,句号。 任何允许这种重新排序的 x86 类系统实际上都不是x86,并且无法正确运行所有 x86 软件。
您可以购买的所有主流 x86 系统实际上都是 x86,具有一致的缓存并遵循 x86 内存模型。
x86 的TSO 内存模型要求所有内核都可以就 Total Store Order 达成一致
因此,相关规则实际上就是内存模型的命名。
- https://homes.cs.washington.edu/~bornholt/post/memory-models.html - 关于 TSO 与 seq-cst 的一些简单内容。即完整排序+存储缓冲区。
- 更好的 x86 内存模型:x86-TSO(扩展版)尝试正式描述 x86 内存模型。它没有提到 NUMA,因为它不相关。
TSO 属性直接来自每个核心,在它们提交到 L1d 之前保持其自己的存储私有,并且具有一致的缓存。
存储缓冲区意味着核心始终在全局可见之前看到自己的存储,除非它像mfence
重新加载之前那样使用 StoreLoad 屏障。
数据在内核之间获取的唯一方法是提交 L1d 缓存以使其全局可见;在其他内核之前不与某些内核共享。(这对于 TSO 来说是必不可少的,无论 NUMA 是什么)。
其余的内存排序规则主要是关于内核内部的重新排序:它确保其存储按程序顺序从存储缓冲区提交到 L1d,并且在任何早期加载已经读取它们的值之后。(以及确保 LoadLoad 排序的其他内部规则,包括内存顺序错误推测管道如果加载顺序推测读取的值在我们被“允许”读取该值之前我们丢失了缓存行。)
只有当核心的相关行处于修改状态时,数据才能从存储缓冲区提交到私有 L1d,这意味着每个其他核心都处于无效状态。这(连同其余的 MESI 规则)保持一致性:在不同的缓存中永远不会有缓存行的冲突副本。 因此,一旦存储已提交缓存,其他核心就无法加载过时的值。(在一个带有 HT 的 Core 上执行的线程之间的数据交换将用于什么?)
一种常见的误解是,存储必须在其他 CPU 停止加载陈旧值之前渗透到系统中。在使用 MESI 维护一致缓存的正常系统中,这是 100% 错误的。 当您谈论 t3 与 t1“更接近”时,您似乎也遭受了这种误解。 如果您有非一致的 DMA,则对于 DMA 设备可能是正确的,正是因为这些 DMA 读取与参与 MESI 协议的 CPU 共享的内存视图不一致。(但现代 x86 也有缓存一致的 DMA。)
事实上,违反 TSO 需要一些非常时髦的行为,其中一个 store 在对所有人可见之前对其他一些内核可见。 PowerPC 在现实生活中为同一物理内核上的逻辑线程执行此操作,以窥探彼此尚未提交到 L1d 缓存的已停用存储。请参阅我对其他线程是否总是以相同的顺序看到不同线程中不同位置的两个原子写入的回答 ? 即使在允许它在纸上的弱有序 ISA 上也很少见。
使用 x86 CPU 但具有非连贯共享内存的系统是(或将是)非常不同的野兽
(我不确定是否存在这样的野兽。)
这更像是紧密耦合的超级计算机集群,而不是单台机器。如果这就是你的想法,那不仅仅是 NUMA,它根本不同,你不能跨不同的一致性域运行普通的多线程软件。
正如维基百科所说,基本上所有的 NUMA 系统都是缓存一致的 NUMA,也就是 ccNUMA。
尽管设计和构建更简单,但非缓存一致的 NUMA 系统在标准冯诺依曼架构编程模型中的编程变得异常复杂
任何使用 x86 CPU 的非一致共享内存系统都不会跨不同的一致性域运行单个内核实例。它可能会有一个自定义 MPI 库和/或其他自定义库,以使用具有显式刷新/一致性的共享内存在一致性域(系统)之间共享数据。
您可以从单个进程启动的任何线程肯定会共享内存的缓存一致视图,并遵守 x86 内存模型,否则您的系统会损坏/存在硬件错误。(我不知道存在任何此类硬件错误并且需要在真实硬件中解决。)
具有一个或多个 Xeon Phi PCIe 卡的系统将每个 Xeon Phi 加速器视为一个单独的“系统”,因为它们与主存储器或彼此不相干,仅在内部相干。请参阅@Hadi 的答案的底部部分,在此示例中数据缓存如何路由对象?. 您可能会将一些工作卸载到 Xeon Phi 加速器,类似于将工作卸载到 GPU 的方式,但这是通过消息传递之类的方式完成的。您不会在主 Skylake(例如)CPU 上运行一些线程,而在 Xeon Phi 上的 KNL 内核上运行同一进程的其他普通线程。如果 Xeon Phi 卡运行的是操作系统,它将是一个独立的 Linux 实例,或者与主机系统上运行的任何东西不同。
x86 NUMA 系统通过在从本地 DRAM 加载之前窥探其他套接字来实现 MESI,以保持缓存一致性。
当然,RFO(读取所有权)请求会广播到其他套接字。
新一代至强引入了越来越多的窥探设置,以权衡不同方面的性能。(例如,更积极的窥探会在套接字之间的链路上花费更多带宽,但可以减少跨套接字的内核间延迟。)
- https://software.intel.com/en-us/articles/intel-xeon-processor-e5-2600-v4-product-family-technical-overview有一个 Broadwell 窥探模式与 LLC 命中延迟的表格,每个本地和远程内存延迟和带宽。
- http://frankdenneman.nl/2016/07/11/numa-deep-dive-part-3-cache-coherency/
可以在四路和更大系统(E7 v1..4)中工作的芯片具有窥探过滤器;Dual-socket E5 v1..4 只是将窥探广播到另一个套接字,占用了我所读到的 QPI 带宽的相当一部分。(这适用于 Skylake-X Xeons、Broadwell 之前的版本。SKX 使用片上网状网络,并且可能总是在套接字之间进行某种窥探过滤。我不确定它的作用。BDW 和更早的版本使用了包容性L3 缓存作为本地内核的监听过滤器,但 SKX 具有非包容性 L3,因此即使在单个套接字中也需要其他东西来进行监听过滤。
AMD 多插槽芯片用于使用 Hypertransport。Zen 在一个插槽内的 4 个核心集群之间使用 Infinity Fabric;我假设它也在套接字之间使用它。
(有趣的事实:多插槽 AMD K10 Opteron 的 Hypertransport 可以在 8 字节边界处产生撕裂,而在单个插槽内,16 字节 SIMD 加载/存储实际上是原子的 。SSE 指令:哪些 CPU 可以执行原子 16B 内存操作?和x86 上的原子性。如果将其算作重新排序,那是一种情况,多插槽比单插槽可以做更多的内存怪异。但这与 NUMA 本身无关;您将拥有相同的东西,所有内存都附加到一个用于 UMA 设置的插座。)
有关的:
另请参阅LOCK XCHG 和 MOV+MFENCE 之间的逻辑和性能有什么区别?对于 xchg 与 mov+mfence。在现代 CPU 上,尤其是 Skylake,在某些测试方式上, mov
+mfence
肯定比 慢xchg
,并且两者都是等效的seq_cst
存储方式。
A release
or relaxed
store 只需要一个 plain mov
,并且仍然具有相同的 TSO 订购保证。
我认为即使是弱排序的 NT 商店,所有核心仍然可以按照他们可以同意的顺序查看。“弱点”的顺序是全局可见的。来自核心的其他加载+存储正在执行它们。
推荐阅读
- javascript - JavaScript - 带有十进制值的正则表达式
- c - 如何通过放入 scanf 的数字数量来定义数组的限制?
- python - 使用 pd.isin() 检查一列中的值是否在另一列的列表中
- charts - 如何在 Apex 图表中同时使用渐变和单色?
- python - 如何向现有字典添加新键并将前一个键作为值附加到在 for 循环中创建的新键:python
- regex - 如何在 google 表格中解析此 URL 以仅显示价格?
- flutter - 像在android启动器中一样在flutter中创建一个文件夹
- swiftui - 如何在 SwiftUI 中隐藏从子视图中的 tabItem 导航的 TabView?
- sql - 在 bigquery 中将罗马数字转换为阿拉伯数字的最佳方法是什么?
- machine-learning - 为什么网络架构的差异会导致名称分类的巨大差异