为什么x86架构的INC指令不是原子性的?

22

我看到过x86的INC指令不是原子操作。我的问题是为什么呢?假设我们在x86-64上增加一个64位整数,我们可以使用一条指令完成,因为INC指令适用于内存变量和寄存器。那么为什么它不是原子的呢?

我已经了解这个问题,INC指令本身是原子的,但是如果该操作涉及到多个处理器核心或线程,则可能会发生竞态条件,导致无法保证原子性。因此,需要使用锁或其他同步机制来确保原子性。


4
如果在它前面加上LOCK,那么它就是原子性的。但通常情况下这并不是我们想要的,因为它会很耗费资源。所以你需要明确表达你想要的是什么。 - Damon
4
"Atomic"并不意味着它只有一条指令,而是指它是一个不可分割的操作。inc和内存操作数并不是这种操作,至少默认情况下不是。 - harold
3个回答

21
为什么呢?处理器核心仍需读取内存位置中存储的值,计算其增量,再将其存回。在读取和存储之间存在延迟,而此时可能会有其他操作影响了该内存位置。
即使采用乱序执行,处理器核心也足够“聪明”,不会引起自身指令的冲突,并且不会在时间间隙中负责修改该内存。然而,另一个核心可能已发出修改该位置的指令,DMA传输可能已影响到该位置,或者其他硬件以某种方式触及了该内存位置。

3
关于“另一个操作”的含义,您应该更加明确。当然,在同一CPU核上不可能发生其他任何操作,只能在其他核心/ CPU或在内存总线上进行其他硬件操作。 - R.. GitHub STOP HELPING ICE

20

现代 x86 处理器作为其执行管道的一部分,将 x86 指令“编译”成一个更低级别的操作集;英特尔称之为 uOps,AMD 称之为 rOps,但它归结为某些类型的单个 x86 指令会被 CPU 中实际的功能单元作为几个步骤执行。


这意味着,例如:

INC EAX

会被执行为一个单独的"mini-op",比如uOp.inc eax(我称之为这个名字 - 它们不会被公开)。
对于其他的操作数,情况会有所不同,例如:

INC DWORD PTR [ EAX ]

但是底层的分解看起来更像:

uOp.load tmp_reg, [ EAX ]
uOp.inc tmp_reg
uOp.store [ EAX ], tmp_reg

因此,它不是原子执行的。另一方面,如果您通过说LOCK INC [ EAX ]来进行前缀,那么这将告诉流水线的“编译”阶段以不同的方式分解以确保满足原子性要求。

当然,其他人提到的原因是速度;如果不总是需要使某些内容具备原子性并且必然降低速度,为什么要这样做呢?


4
“Mini-op”分解与原子性无关,因为单个CPU核心不能在指令执行过程中被打断。实际上,在单核机器上,没有锁前缀的“inc”操作是完全原子的。只有当其他核心(或更隐晦的情况下,总线上的其他硬件)可以访问内存时,锁前缀才是必要的。 - R.. GitHub STOP HELPING ICE
1
@R..:如果这样说,单核上的任何修改内存CPU操作都是原子的,无论如何完成。但即使是单核机器今天也不再是“单一”的了,因为总线主控DMA/内存总线与外围设备共享,确保缓存一致性和原子性问题的存在。始终存在多个内存总线客户端。因此,即使它们作为“单个”CPU指令的一部分发生,加载/存储在内存总线级别上也总是被分解的。必须断言原子性(独占内存总线访问);CPU不能将修改内存作为加载/更改/存储执行,而必须用总线锁定/解锁括起来。 - FrankH.
1
通常情况下,总线上没有其他设备会触及您程序的内存;这只是硬件驱动程序中才会发生的特殊情况。在大多数实际情况下,单核机器上的加载-修改-写入指令是原子性的,事实上,许多代码(包括Linux)在构建用于非SMP目标的代码时省略了锁定前缀,因为从历史上看,这样做更快,并且对于该使用情况来说是完全安全的。 - R.. GitHub STOP HELPING ICE
2
@R..:如果你说在单核机器上,没有使用lock的非原子性对于需要同步原语的用例没有任何影响,那么我同意。但我不同意这使得“默认”是原子性 - 在单核上,你只是不需要原子性来进行同步。 - FrankH.
1
@FrankH.:inc [mem] 对于同一核上的上下文切换是原子性的:整个指令在引发上下文切换的中断之前或之后执行。如果指令的某些但不是全部uop已经执行,则在进行中断时将取消所有uop,以保留与中断相关的原子性假象。只有MMIO设备或DMA读取可以观察到非原子性,而不是同一CPU上的其他代码。重要的是相对于哪些观察者的原子性,因为逻辑分析仪当然会看到单独的加载/存储(如果未缓存)。 - Peter Cordes
显示剩余3条评论

1

来自Agner Fog软件优化资源instruction_tables.pdf(1996-2017),你真的不需要保证原子操作,除非你需要它:

带有LOCK前缀的指令具有长延迟,这取决于缓存组织和可能的RAM速度。如果有多个处理器或核心或直接内存访问(DMA)设备,则所有锁定指令都将锁定一个缓存行以进行独占访问,这可能涉及RAM访问。即使在单处理器系统上,LOCK前缀的成本通常也超过一百个时钟周期。对于带有内存操作数的XCHG指令也是如此。


2
这些信息肯定已经过时了;在我测试的一台机器上,整个互斥锁/解锁周期不到90个周期,并涉及多个带锁前缀的操作和rdtsc开销。在两个rdtsc之间进行单个锁inc指令的测试中,我甚至无法测量它需要任何时间(与nop相同的时间)。在现代CPU上,除非内存当前与其他核心共享,否则似乎锁定前缀根本不会增加时间。 - R.. GitHub STOP HELPING ICE
@R.. - x86-64自2003年以来就已经可用,因此这可能是一个笼统的说法。我想知道它对待处理中断/上下文切换会有什么影响。 - Brett Hale
3
@R..好的,小于90和大于100并不相差太远 :-) - Gunther Piez
1
那90纳秒包括:(1)rdtsc时间(可能是40纳秒,我忘记了确切的成本),(2)函数调用和返回开销,(3)一个但至少两个带锁前缀的指令(用于获取和释放锁)。项目(1)和(2)占据了大部分时间,剩下的(3)接近零... - R.. GitHub STOP HELPING ICE
instruction_tables.pdf中的这个通知已经很旧了。即使在http://www.agner.org/optimize/microarchitecture.pdf中,对于Nehalem也有:“*线程同步原语,例如LOCK XCHG指令,比以前的处理器快得多*”。只有在早期的共享总线型CPU中,LOCK才是总线的外部引脚并全局锁定内存;现代多核CPU具有集成内存控制器IMC和点对点插座通道(单插座也是如此),使用缓存一致性协议来执行原子操作,并且它是在缓存层次结构的某一层中完成的。哪一层呢? - osgx

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接