mov + mfence在NUMA上安全吗?

时间:2019-02-12 14:46:02

标签: c++ x86 memory-model numa stdatomic

我看到g ++为mov生成了一个简单的x.load(),为mov生成了一个mfence + 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有机会比计划的要早观察到写入吗?

1 个答案:

答案 0 :(得分:6)

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

NUMA甚至与此无关。一个多核单路x86系统已经可以执行x86内存模型所允许的尽可能多的内存重新排序。 (也许不那么频繁或使用较小的时间窗口。)


TLDR.1:您似乎误解了mfence的作用。这是运行它的核心的本地障碍(包括StoreLoad,唯一的重新排序x86确实对非NT加载/存储没有障碍)。即使x86的订购不严格,这也完全无关紧要:我们正在寻找来自不同内核的每个存储器1个,因此对单个内核的操作进行了排序。彼此都没关系。

({mfence仅使该核心等待执行任何加载,直到其存储在全局可见为止。mfence等待它时,存储在提交时没有什么特别的事情。Does a memory barrier ensure that the cache coherence has been completed?。 )


TL:DR.2: Will two atomic writes to different locations in different threads always be seen in the same order by other threads? C ++允许不同的线程不同意宽松或发行商店的商店订单(当然要获取负载以排除LoadLoad的重新排序),但是不能与seq_cst一起使用。

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

您可以购买的所有主流x86系统 实际上是具有连贯缓存并遵守x86内存模型的x86。


x86的TSO memory model要求所有内核都可以就总存储订单达成共识

因此,相关规则实际上就是内存模型的命名方式。

TSO属性直接源自每个内核将其自己的存储保持私有状态,直到它们提交到L1d为止,并遵循相干缓存。

存储缓冲区意味着,除非在重新加载之前使用mfence之类的StoreLoad屏障,否则内核始终会在全局可见之前看到自己的存储。

数据在内核之间获取的唯一方法是通过提交L1d缓存使其全局可见;没有与其他核心共享。 (这对于TSO至关重要,而与NUMA无关)。

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

仅当该核心的相关行处于“已修改”状态时,数据才能从存储缓冲区提交到私有L1d,这意味着每个其他核心都使其处于“无效”状态。这(与其余的MESI规则一起)保持了一致性:在不同的缓存中永远不会有冲突的缓存行副本。 因此,一旦商店承诺进行缓存,其他任何核心都无法加载过时的值。What will be used for data exchange between threads are executing on one Core with HT?

一个常见的误解是,存储必须在其他CPU停止加载过时的值之前渗透整个系统。在使用MESI维护一致性缓存的普通系统中,这是100%的错误。 当您谈到t3与t1“更接近”时,您似乎也在遭受这种误解。如果您使用的是非相干DMA,则对于DMA设备来说可能是正确的,正是因为这些DMA读取将与参与MESI协议的CPU共享的内存视图不一致。 (但是现代的x86也具有缓存一致的DMA。)

实际上,违反TSO要求采取一些非常时髦的行为,即在其他存储核心对所有人可见之前,存储对其他一些核心才可见。 PowerPC在现实生活中对同一物理核心上的逻辑线程执行此操作侦听彼此尚未提交到L1d缓存的已退休存储。在Will two atomic writes to different locations in different threads always be seen in the same order by other threads?上查看我的答案,即使是在纸张上允许使用的顺序较弱的ISA上也很少见。


使用x86 CPU但具有非一致性共享内存的系统是(或者可能是)完全不同的野兽

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

与单台计算机相比,它更像是紧密耦合的超级计算机集群。如果这就是您的想法,那不只是NUMA,它的根本区别还在于,您不能在不同的一致性域中运行普通的多线程软件。

As Wikipedia says,基本上所有NUMA系统都是与缓存相关的NUMA,也就是ccNUMA。

  

尽管设计和构建更简单,但非缓存一致的NUMA系统在标准von Neumann体系结构编程模型中编程变得异常复杂

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

您可以从一个进程中启动的任何线程肯定会共享一个缓存一致性的内存视图,并遵守x86内存模型,否则您的系统已损坏/出现硬件错误。 (我不知道是否存在任何此类硬件错误,需要在实际硬件中解决。)

具有一个或多个Xeon Phi PCIe卡的系统将每个Xeon Phi加速器视为一个单独的“系统”,因为它们与主内存或彼此不相关,而仅内部相干。请参阅@Hadi在How do data caches route the object in this example?上的答案的底部。您可能会将某些工作卸载到Xeon Phi加速器,类似于将工作卸载到GPU的工作,但是这是通过诸如消息传递之类的方式完成的。您不会不会在Skylake主CPU上运行某些线程,而在Xeon Phi的KNL内核上运行同一进程的其他普通线程。如果Xeon Phi卡运行的是操作系统,则它将是Linux的单独实例,或者是主机系统上运行的任何实例。


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

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

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

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

AMD多插槽芯片过去用于Hypertransport。 Zen在一个插槽中的4个核的群集之间使用Infinity Fabric;我假设它也在套接字之间使用了。

(有趣的事实:多插槽AMD K10 Opteron的Hypertransport可能会在8字节边界处产生撕裂,而在单个插槽中16字节SIMD加载/存储实际上是原子的。SSE instructions: which CPUs can do atomic 16B memory operations?和{{3} }。如果您将其视为重新排序,那么在这种情况下,多插槽比单插槽可以处理更多的内存异常,但这与NUMA本身无关;将所有内存附加到一个插槽上的方法相同UMA设置。)


相关

另请参阅Atomicity on x86中xchg与mov + mfence的重复链接。在现代CPU(尤其是Skylake)上,对于某些测试方式,mov + mfence绝对比xchg慢,并且两者都是进行seq_cst存储的等效方式。

releaserelaxed存储只需要普通的mov,并且仍然具有相同的TSO排序保证。

我认为,即使是顺序较弱的NT商店,仍然可以按照所有内核的顺序在所有内核中看到。 “弱点”按顺序成为全局可见的wrt。其他核心的加载和存储。