c – x86上的原子性
作者:互联网
8.1.2 Bus Locking
Intel 64 and IA-32 processors provide a LOCK# signal that is asserted
automatically during certain critical memory operations to lock the
system bus or equivalent link. While this output signal is asserted,
requests from other processors or bus agents for control of the bus
are blocked. Software can specify other occasions when the LOCK
semantics are to be followed by prepending the LOCK prefix to an
instruction.
它来自英特尔手册,第3卷
听起来内存上的原子操作将直接在内存(RAM)上执行.我很困惑,因为当我分析装配输出时,我看到“没什么特别的”.基本上,为std :: atomic< int>生成的程序集输出X; X.load()只放置“额外”的mfence.但是,它负责正确的内存排序,而不是原子性.如果我理解正确,X.store(2)只是mov [某处],$2.就这样.它似乎没有“跳过”缓存.我知道将对齐(例如int)移动到内存是原子的.但是,我很困惑.
所以,我提出了疑问,但主要问题是:
CPU如何在内部实现原子操作?
解决方法:
It sounds like the atomic operations on memory will be executed directly on memory (RAM).
不,只要系统中的每个可能的观察者都将操作视为原子操作,操作就只能涉及缓存.对于原子读取 – 修改 – 写入操作(如lock add [mem],eax,尤其是未对齐的地址)来说,满足此要求要困难得多,这就是CPU可能会断言LOCK#信号的时候.您仍然不会在asm中看到更多内容:硬件为锁定指令实现了ISA所需的语义.
虽然我怀疑现代CPU上有一个物理外部LOCK#引脚,内存控制器内置于CPU,而不是单独的northbridge chip.
std::atomic<int> X; X.load()
puts only “extra” mfence.
对于seq_cst加载,编译器不具有MFENCE.我想我读过MSVC确实为此发出了MFENCE(可能是为了防止对不受保护的NT存储进行重新排序?),但它不再是:我刚刚测试了MSVC 19.00.23026.0.在this program that dumps its own asm in an online compile&run site的asm输出中查找foo和bar.
我认为我们不需要围栏的原因是x86内存模型不允许LoadStore和LoadLoad重新排序.早期(非seq_cst)存储仍然可以延迟到seq_cst加载之后,因此它与使用独立的std :: atomic_thread_fence(mo_seq_cst)不同;在X.load之前(mo_acquire);
If I understand properly the
X.store(2)
is justmov [somewhere], 2
不,seq_cst商店确实需要完整的内存屏障指令,以禁止StoreLoad reordering which could otherwise happen.
MSVC的商店asm与clang’s相同,使用xchg进行存储,使用相同的指令进行内存屏障. (在某些CPU上,尤其是AMD,作为屏障的锁定指令可能比MFENCE便宜,因为IIRC AMD为MFENCE记录了额外的序列化管道语义(用于指令执行,而不仅仅是内存排序).
这个问题看起来像你早期的Memory Model in C++ : sequential consistency and atomicity的第2部分,你问过:
How does the CPU implement atomic operations internally?
正如您在问题中指出的那样,原子性与任何其他操作的排序无关. (即memory_order_relaxed).它只是意味着操作是作为单个不可分割的操作发生的,而不是作为多个部分发生在部分之前和部分之后.
您可以“免费”获得原子性,没有额外的硬件用于对齐的加载或存储,最大可达内核,内存和I / O总线(如PCIe)之间的数据路径.即,在各种级别的高速缓存之间,以及在单独的核的高速缓存之间.在现代设计中,存储器控制器是CPU的一部分,因此即使访问存储器的PCIe设备也必须通过CPU的系统代理. (这甚至让Skylake的eDRAM L4(在任何桌面CPU中都不可用:()作为内存端缓存(不像Broadwell,它用作L3 IIRC的受害者缓存),位于内存和系统中的其他所有内容之间,因此它甚至可以缓存DMA).
这意味着CPU硬件可以做任何必要的事情,以确保存储或负载相对于系统中可以观察它的任何其他东西是原子的.这可能不是很多,如果有的话. DDR存储器使用足够宽的数据总线,64位对齐的存储器确实在同一周期内通过存储器总线电流到DRAM. (有趣的是,但并不重要.像PCIe这样的串行总线协议不会阻止它成为原子,只要单个消息足够大.而且由于内存控制器是唯一可以直接与DRAM通信的东西,它在内部的作用并不重要,只是它与CPU的其余部分之间的传输大小.但无论如何,这是“免费”部分:不需要临时阻止其他请求来保持原子转移原子.
x86 guarantees that aligned loads and stores up to 64 bits are atomic,但没有更广泛的访问.低功耗实现可以自由地将向量加载/存储分解为64位块,例如P6从PIII到Pentium M.
原子操作发生在缓存中
请记住,原子只是意味着所有观察者都认为它已发生或未发生,从未发生过部分发生.没有要求它实际上立即到达主存储器(或者根本不会很快被覆盖).原子修改或读取L1高速缓存足以确保任何其他核心或DMA访问将看到对齐的存储或加载作为单个原子操作发生.如果这种修改在商店执行很久之后发生(例如,在商店退休之前由无序执行延迟),那就没问题了.
像Core2这样具有128位路径的现代CPU通常具有原子SSE 128b加载/存储,超出了x86 ISA所保证的范围.但请注意有趣的异常on a multi-socket Opteron probably due to hypertransport.这证明原子修改L1高速缓存不足以为比最窄数据路径(在这种情况下不是L1高速缓存和执行单元之间的路径)更宽的存储提供原子性.
对齐非常重要:跨越缓存行边界的加载或存储必须在两个单独的访问中完成.这使它成为非原子的.
x86 guarantees that cached accesses up to 8 bytes are atomic as long as they don’t cross an 8B boundary关于AMD /英特尔. (或者仅针对P6及更高版本的Intel,不要跨越缓存行边界).这意味着整个缓存行(现代CPU上的64B)在英特尔上原子地传输,即使它比数据路径(Haswell / Skylake上的L2和L3之间的32B)更宽.这种原子性在硬件中并非完全“自由”,并且可能需要一些额外的逻辑来防止负载读取仅部分传输的缓存行.虽然缓存行传输仅在旧版本无效后才会发生,但是当发生传输时,核心不应该从旧副本读取. AMD可以在更小的边界上实践,可能是因为使用了可以在缓存之间传输脏数据的MESI的不同扩展.
对于更广泛的操作数,例如原子地将新数据写入结构的多个条目,您需要使用锁来保护它,所有访问它的方面都是如此. (您可以使用x86 lock cmpxchg16b和重试循环来执行原子16b存储.请注意there’s no way to emulate it without a mutex.)
原子读 – 修改 – 写是更难的地方
相关:我在Can num++ be atomic for ‘int num’?的答案详细介绍了这一点.
每个内核都有一个私有L1缓存,与所有其他内核(使用MOESI协议)保持一致.高速缓存行在高速缓存和主存储器的级别之间以块的大小从64位到256位传输. (这些传输实际上可能是整个缓存行粒度的原子?)
要执行原子RMW,核心可以将一行L1高速缓存保持在已修改状态,而不接受对加载和存储之间受影响的高速缓存行的任何外部修改,系统的其余部分将将操作视为原子操作. (因此它是原子的,因为通常的乱序执行规则要求本地线程将其自己的代码视为按程序顺序运行.)
它可以通过在原子RMW正在运行时不处理任何缓存一致性消息来实现这一点(或者一些更复杂的版本允许其他操作更多并行).
未对齐的锁定操作是一个问题:我们需要其他内核来查看对两个缓存行的修改是作为单个原子操作发生的.这可能需要实际存储到DRAM,并采取总线锁定. (AMD的优化手册说,当缓存锁定不足时,这就是他们的CPU上发生的情况.)
标签:memory-barriers,c,x86,multithreading,atomic 来源: https://codeday.me/bug/20190918/1811758.html