mov + mfence 在 NUMA 上安全吗?

Is mov + mfence safe on NUMA?

我看到 g++ 为 x.load() 生成了一个简单的 mov,为 x.store(y) 生成了一个简单的 mov+mfence。 考虑这个经典示例:

#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 "trickle down" 对其各自内存视图的写入顺序。特别考虑一个 NUMA 架构,其中 t3 恰好生活在 "closer" 到 t1 和 t4 是 "closer" 到 t2。 是否可以发生 t1 或 t2 "flushes prematurely" 的存储缓冲区甚至在到达 mfence 之前,然后 t3 或 t4 有机会比计划更快地观察到写入?

是的,很安全。您不需要为 NUMA 安全代码启用特殊的编译器选项,因为 asm 不需要不同。

NUMA 甚至与此无关;多核单插槽 x86 系统已经可以进行 x86 内存模型允许的尽可能多的内存重新排序。 (可能更少或时间更短 windows。)


TLDR.1:您似乎误解了 mfence 的作用。 运行 它是内核的局部屏障(包括 StoreLoad,唯一的重新排序 x86 确实允许非 NT loads/stores 没有屏障)。这与此完全无关,即使 x86 是弱排序的:我们正在查看每个来自不同内核的 1 个存储,因此单个内核操作的排序 wrt。彼此无所谓。

(mfence 只是让该核心等待执行任何加载,直到它的存储在全局可见之后。当存储在 mfence 等待它时提交时没有什么特别的发生。.)


TL:DR.2: C++ 允许不同的线程不同意放松或释放存储的存储顺序(当然获取负载以排除LoadLoad 重新排序),但不使用 seq_cst.

在可能的架构上,编译器需要在 seq-cst 存储上设置额外的屏障来防止它。 在 x86 上,不可能,完全停止。任何允许这种重新排序的类 x86 系统实际上不会 x86,并且无法正确 运行 所有 x86 软件。

您可以购买的所有主流 x86 系统实际上是 x86,具有一致的缓存并遵循 x86 内存模型。


x86 的 TSO memory model 要求所有内核都可以就 Total Store Order

达成一致

所以相关的规则就是内存模型的命名。

TSO 属性 直接遵循每个核心保持其自己的存储私有直到它们提交到 L1d,以及具有一致的缓存。

存储缓冲区意味着核心总是在它们变得全局可见之前看到自己的存储,除非它在重新加载之前使用像 mfence 这样的 StoreLoad 屏障。

数据在内核之间传输的唯一方法是提交到 L1d 缓存以使其全局可见;在其他核心之前不与某些核心共享。 (无论 NUMA 是多少,这对于 TSO 都是必不可少的)。

其余的内存排序规则主要是关于核心内部的重新排序:它确保其存储按程序顺序从存储缓冲区提交到 L1d,并且在任何较早的加载已经读取了它们的值之后。 (以及确保 LoadLoad 排序的其他内部规则,包括如果加载顺序推测读取的值在我们 "allowed" 读取该值之前我们丢失了缓存行,则内存顺序错误推测管道刷新。)

当该核心的相关行处于修改状态时,数据只能从存储缓冲区提交到私有 L1d,这意味着其他所有核心都处于无效状态。这(与 MESI 规则的其余部分一起)保持一致性:不同缓存中的缓存行永远不会有冲突的副本。 因此,一旦存储提交到缓存,其他核心就无法加载陈旧值。 ()

一个常见的误解是,存储必须在其他 CPU 停止加载陈旧值之前渗透到系统中。在使用 MESI 维护一致缓存的普通系统中,这是 100% 错误的。 当你谈论 t3 是 "closer" 到 t1 时,你似乎也有这种误解。 如果你有非相干 DMA,那么对于 DMA 设备来说可能是这样,正是因为这些 DMA 读取与参与 MESI 协议的 CPUs 共享的内存视图不一致。 (但现代 x86 也有缓存一致的 DMA。)

事实上,违反 TSO 需要一些非常时髦的行为,其中存储在对所有内核可见之前对其他一些内核可见。 PowerPC 在现实生活中为逻辑线程执行此操作在同一个物理核心上互相窥探尚未提交给 L1d 缓存的 retired 存储。请参阅我在 上的回答即使在允许它在纸上的弱排序 ISA 上也是罕见的。


使用 x86 CPUs 但共享内存不一致的系统是(或将是)非常不同的野兽

(我不确定是否存在这样的野兽。)

这更像是紧密耦合的超级计算机集群,而不是单台机器。如果这就是您的想法,那不仅仅是 NUMA,它根本不同,您不能 运行 跨不同一致性域的普通多线程软件。

As Wikipedia says,基本上所有的 NUMA 系统都是缓存一致的 NUMA,又名 ccNUMA。

Although simpler to design and build, non-cache-coherent NUMA systems become prohibitively complex to program in the standard von Neumann architecture programming model

任何使用 x86 CPUs 的非一致性共享内存系统都不会 运行跨不同一致性域的单个内核实例。它可能有一个自定义 MPI 库 and/or 其他自定义库使用具有显式刷新/一致性的共享内存在一致性域(系统)之间共享数据。

你可以从单个进程启动的任何线程肯定会共享一个缓存一致的内存视图,并遵守 x86 内存模型,否则你的系统就会崩溃/有硬件错误。 (我不知道在真实硬件中存在任何此类硬件错误,需要解决这些错误。)

具有一个或多个 Xeon Phi PCIe 卡的系统将每个 Xeon Phi 加速器视为一个单独的 "system",因为它们与主内存或彼此不一致,仅在内部一致。请参阅@Hadi 在 上的回答的底部部分。您可能会将一些工作卸载到 Xeon Phi 加速器,类似于将工作卸载到 GPU 的方式,但这是通过消息传递等方式完成的。你会 not 在主 Skylake 上有一些线程 运行ning(例如)CPU 和同一进程的其他普通线程 运行ning Xeon Phi 上的 KNL 内核。如果 Xeon Phi 卡是 运行ning 一个 OS,它将是 separate Linux 的实例或来自 运行 的任何东西在主机系统上 ning。


x86 NUMA 系统通过在从本地 DRAM 加载之前侦听其他套接字来实现 MESI,以保持缓存一致性。

当然,RFO(读取所有权)请求会广播到其他套接字。

新一代 Xeon 引入了越来越多的侦听设置以权衡不同方面的性能。 (例如,更积极的监听会在套接字之间的 link 上消耗更多带宽,但可以减少套接字之间的内核间延迟。)

可以在 Quad-socket 和更大系统 (E7 v1..4) 中工作的芯片具有监听过滤器;双插槽 E5 v1..4 只是向另一个插槽广播侦听,使用了我所阅读的相当一部分 QPI 带宽。 (这是针对 Skylake-X 之前的 Xeons、Broadwell 和更早版本。SKX 在芯片上使用网状网络,并且可能总是在套接字之间进行某种监听过滤。我不确定它的作用。BDW 和更早版本使用了包容性L3 缓存作为本地内核的侦听过滤器,但 SKX 具有非包容性 L3,因此即使在单个插槽中也需要其他东西来进行侦听过滤。

AMD 多插槽芯片用于使用 Hypert运行sport。 Zen 在一个插槽内的 4 个内核集群之间使用 Infinity Fabric;我假设它也在套接字之间使用它。

(有趣的事实:多插槽 AMD K10 Opteron 的 Hypert运行 运动可能会在 8 字节边界处产生撕裂,而在单个插槽中,16 字节 SIMD loads/stores 实际上是原子的。 SSE instructions: which CPUs can do atomic 16B memory operations? and 。如果您将其算作重新排序,那么在这种情况下,多插槽可以比单插槽执行更多的内存怪异操作。但这与 NUMA 本身无关;连接所有内存也会有同样的事情到一个用于 UMA 设置的插座。)


相关:

另请参阅 What is the difference in logic and performance between LOCK XCHG and MOV+MFENCE? 中 xchg 与 mov+mfence 的重复 link。在现代 CPU 上,尤其是 Skylake,mov+mfence 对于某些测试方式来说肯定比 xchg 慢,并且两者都是等效的 seq_cst 商店。

releaserelaxed 商店只需要一个普通的 mov,并且仍然有相同的 TSO 订购 gua运行tees。

我认为即使是弱排序的 NT 存储仍然被所有核心以他们可以同意的顺序看到。 "weakness" 的顺序是全局可见的。来自核心的其他加载+存储正在执行它们。