提问者:小点点

x86上加载和存储的原子性


Intel 64和IA-32处理器提供LOCK#信号,该信号在某些关键内存操作期间自动断言,以锁定系统总线或等效链路。当此输出信号被断言时,来自其他处理器或总线代理的控制总线的请求将被阻止。软件可以指定在LOCK语义学之后将LOCK前缀附加到指令的其他情况。

它来自英特尔手册第3卷

听起来好像对内存的原子操作会直接在内存(RAM)上执行,我很困惑,因为我在分析汇编输出时看到“没什么特别的”。

基本上,为st生成的程序集输出d::atomic

我知道将对齐整数(例如整数)移动到内存是原子的。然而,我很困惑。

因此,我提出了我的疑问,但主要问题是:

CPU内部如何实现原子操作?


共2个答案

匿名用户

听起来内存上的原子操作将直接在内存(RAM)上执行。

不,只要系统中的每个可能的观察者都将操作视为原子操作,则该操作只能涉及缓存。

满足这一要求对于原子读-修改-写操作(如lock add[mem]、eax,尤其是地址不对齐的情况下)要困难得多,这时CPU可能会断言LOCK#信号。您仍然不会在asm中看到更多:硬件实现了locked指令的ISA语义学。

尽管我怀疑现代CPU上是否有物理外部锁#pin,其中内存控制器内置于CPU,而不是单独的北桥芯片中。

std::atomic

编译器不MFENCEseq_cst负载。

我想我读到过旧的MSVC曾经为此发出过MFENCE(也许是为了防止用未隔离的NT存储重新排序?或者代替存储?)。但现在不再是了:我测试了MSVC 19.00.23026.0。在这个程序的asm输出中寻找foo和bar,该程序在在线编译中转储了自己的asm

这里我们不需要栅栏的原因是x86内存模型不允许LoadStore和LoadLoad重新排序。早期(非seq_cst)存储仍然可以延迟到seq_cst加载之后,因此它不同于在X. load(mo_acquire);之前使用独立的std::atomic_thread_fence(mo_seq_cst);

如果我正确理解X.store(2)只是mov[某处],2

这与您的想法是一致的,即加载需要mgrid;seq_cst加载或存储中的一个或另一个需要一个完整的屏障来防止禁止StoreLoad重新排序,否则可能会发生这种情况。

在实践中,编译器开发人员选择了便宜的负载(mov)/昂贵的存储(mov mgrid),因为负载更常见。C 11到处理器的映射。

(x86内存排序模型是程序顺序加上带有存储转发的存储缓冲区(另请参阅)。这使得mo_acquiremo_release在asm中是免费的,只需要阻止编译时重新排序,并让我们选择是在加载还是存储上设置MFENCE完全屏障。)

所以seq_cst存储要么是movmgrid要么是xchg。为什么具有顺序一致性的std::atomic存储使用XCHG?讨论xchg在某些CPU上的性能优势。AMD,MFENCE(IIRC)被证明具有额外的序列化管道语义学(用于指令执行,而不仅仅是内存排序),可以阻止乱序执行,在实践中的一些Intel CPU(Skylake)上也是如此。

MSVC对存储的asm与clang的相同,使用xchg用相同的指令做存储内存屏障。

原子发布或放松存储可以只是mov,它们之间的区别只是允许多少编译时重新排序。

这个问题看起来像您之前的C内存模型的第2部分:顺序一致性和原子性,您在其中问道:

正如你在问题中指出的,原子性与任何其他操作的排序无关。(即memory_order_relaxed)。它只是意味着操作作为一个不可分割的操作发生,因此得名,而不是作为多个部分发生,部分发生在其他事情之前,部分发生在其他事情之后。

您可以“免费”获得原子性,无需额外的硬件来对齐负载或存储内核、内存和PCIe等I/O总线之间的数据路径大小。即在不同级别的缓存之间,以及在单独内核的缓存之间。内存控制器是现代设计中CPU的一部分,因此即使是访问内存的PCIe设备也必须通过CPU的系统代理。(这甚至可以让Skylake的eDRAM L4(在任何桌面CPU中都不可用:()作为内存侧缓存(不像Broadwell,它将其用作L3IIRC的受害者缓存),位于内存和系统中其他所有内容之间,因此它甚至可以缓存DMA)。

这意味着CPU硬件可以做任何必要的事情来确保存储或加载相对于系统中可以观察到的任何其他东西都是原子的。如果有的话,这可能并不多。DDR内存使用足够宽的数据总线,以至于64位对齐的存储确实在同一个周期内通过内存总线电到达DRAM。(有趣的事实,但不重要。像PCIe这样的串行总线协议不会阻止它成为原子的,只要一条消息足够大。由于内存控制器是唯一可以直接与DRAM对话的东西,所以它在内部做什么并不重要,重要的是它和CPU)其余部分之间传输的大小。但无论如何,这是“免费”部分:不需要临时阻塞其他请求来保持原子传输原子。

x86保证最多64位的对齐加载和存储是原子的,但不是更广泛的访问。低功耗实现可以自由地将向量加载/存储分解为64位块,就像P6从PIII到Pentium M所做的那样。

请记住,原子只是意味着所有观察者都认为它已经发生或没有发生,从未部分发生。不需要它立即到达主存储器(或者根本不需要,如果很快被覆盖)。原子修改或读取L1缓存足以确保任何其他核心或DMA访问将看到对齐的存储或加载作为单个原子操作发生。如果这种修改发生在存储执行后很久(例如,由于无序执行而延迟到存储退役)。

像Core2这样具有128位路径的现代CPU通常具有原子SSE128b加载/存储,超出了x86ISA所保证的范围。但是请注意多套接字Opteron上有趣的例外可能是由于超传输。这证明了原子修改L1缓存不足以为比最窄数据通路更宽的存储提供原子性(在这种情况下,这不是L1缓存和执行单元之间的路径)。

对齐很重要:跨越缓存行边界的加载或存储必须在两次单独的访问中完成。这使得它是非原子的。

x86保证缓存的8个字节的访问是原子的,只要它们不跨越AMD/Intel上的8B边界。(或者仅对于P6及更高版本的Intel,不要跨越缓存线边界)。这意味着整个缓存线(64B在现代CPU上)在Intel上是原子传输的,尽管这比数据路径(32BHaswell/Skylake上的L2和L3之间)宽。这种原子性在硬件中并不完全“自由”,可能需要一些额外的逻辑来防止负载读取仅部分传输的缓存线。虽然缓存行传输只发生在旧版本无效之后,所以当传输发生时,核心不应该从旧副本中读取。AMD可以在较小的边界上撕裂,可能是因为使用了不同的MESI扩展,可以在缓存之间传输脏数据。

对于更广泛的操作数,例如将新数据原子地写入结构的多个条目,您需要使用所有访问它的锁来保护它。(您可以使用带有重试循环的x86lock cmpxchg16b来执行原子16b存储。请注意,没有互斥锁就无法模拟它。)

原子读-修改-写是越来越难的地方

相关:我对“int num”的num可以是原子吗?的回答对此进行了更详细的介绍。

每个内核都有一个私有的L1缓存,它与所有其他内核(使用MOESI协议)相一致。缓存线在缓存和主存之间传输,大小从64位到256位不等。(这些传输实际上可能是整个缓存线颗粒度的原子?)

要做一个原子RMW,一个核心可以保持一行L1缓存处于修改状态,而不接受对加载和存储之间受影响的缓存行的任何外部修改,系统的其余部分将看到操作是原子的。(因此它是原子的,因为通常的无序执行规则要求本地线程将自己的代码视为已按程序顺序运行。)

它可以通过在原子RMW正在运行时不处理任何缓存一致性消息来做到这一点(或者一些更复杂的版本,它允许其他操作具有更多并行性)。

未对齐的锁是一个问题:我们需要其他内核来查看对两个缓存行的修改作为单个原子操作发生。这可能需要实际存储到DRAM,并采取总线锁。(AMD的优化手册说,当缓存锁不够时,他们的CPU上会发生这种情况。)

匿名用户

LOCK#信号(cpu包/套接字的pin)用于旧芯片(用于LOCK前缀原子操作),现在有缓存锁。对于更复杂的原子操作,如。交换。fetch_add您将使用LOCK前缀或其他类型的原子指令(cmpxchg/8/16?)进行操作。

同手册,系统编程指南部分:

在奔腾4、英特尔至强和P6系列处理器中,锁定操作是用缓存锁或总线锁处理的。如果内存访问是可缓存的并且只影响单个缓存行,则会调用缓存锁,并且在操作期间不会锁定系统总线和系统内存中的实际内存位置

您可以查看Paul E. McKenney的论文和书籍:*现代微处理器中的内存排序,2007*内存障碍:软件黑客的硬件视图,2010*成绩手册,“并行编程难吗,如果难,你能做些什么?”

和*Intel 64架构内存订购白皮书,2007年。

x86/x86_64需要内存屏障来防止负载重新排序。从第一篇论文开始:

x86(… AMD64与x86兼容…)由于x86 CPU提供进程排序,因此所有CPU都同意给定CPU写入内存的顺序,因此smp_wmb()原语是CPU[7]的无操作。但是,需要编译器指令来防止编译器执行会导致跨smp_wmb()原语重新排序的优化。

另一方面,x86 CPU传统上对加载没有排序保证,因此smp_mb()smp_rmb()原语扩展为lock; addl。这个原子指令充当加载和存储的屏障。

什么是读取内存障碍(来自第二篇论文):

这样做的效果是,读取内存屏障命令仅加载执行它的CPU,因此在读取内存屏障之前的所有加载似乎在读取内存屏障之后的任何加载之前已经完成。

例如,来自《Intel 64架构内存订购白皮书》

Intel 64内存排序保证对于以下每个内存访问指令,组成内存操作似乎作为单个内存访问执行,而不管内存类型如何:…读取或写入地址在4字节边界上对齐的双字(4字节)的指令。

Intel 64内存排序遵循以下原则:1.负载不会与其他负载一起重新排序。…5.在多处理器系统中,内存排序遵循因果关系(内存排序尊重传递可见性)。… Intel 64内存排序确保按程序顺序看到负载

另外,定义的mgrid:http://www.felixcloutier.com/x86/MFENCE.html

对在MFENCE指令之前发出的所有从内存加载和存储到内存指令执行序列化操作。此序列化操作保证在MFENCE指令之前的每个加载和存储指令按程序顺序在任何跟随MFENCE指令的加载或存储指令之前全局可见。