以下程序集是原子的,如果不是,为啥?

Posted

技术标签:

【中文标题】以下程序集是原子的,如果不是,为啥?【英文标题】:Is the following Assembly Atomic, If not, Why?以下程序集是原子的,如果不是,为什么? 【发布时间】:2016-02-11 06:12:43 【问题描述】:
addl, $9, _x(%rip)

_x 是一个全局变量。本质上,我不确定在这种情况下如何实现添加到全局变量,以及在多处理器系统中这条线是否存在固有的竞争条件。

【问题讨论】:

【参考方案1】:

正如duskwuff 指出的那样,您需要一个lock 前缀。

原因是这样的:

addl $9,_x(%rip)

从内存系统的角度来看实际上是三个“微操作”[这里%eax只是为了说明--从未真正使用过]:

mov     _x(%rip),%eax
addl    $9,%eax
mov     %eax,_x(%rip)

这是一个有效的事件序列。这是由lock 前缀保证的。最后,_x 是 18 岁:

# this is a valid sequence

# cpu 1                         # cpu 2
mov     _x(%rip),%eax
addl    $9,%eax
mov     %eax,_x(%rip)
                                mov     _x(%rip),%eax
                                addl    $9,%eax
                                mov     %eax,_x(%rip)

但是,如果没有lock,我们可以得到:

# this is an invalid sequence

# cpu 1                         # cpu 2
mov     _x(%rip),%eax
                                mov     _x(%rip),%eax
addl    $9,%eax                 addl    $9,%eax
mov     %eax,_x(%rip)
                                mov     %eax,_x(%rip)

最后,_x 将是 9。序列的进一步混乱可能会产生 18。因此,根据两个 CPU 上微操作之间的确切顺序,我们可以任一个 9 或 18。

我们可以让它变得更糟。如果 CPU 2 添加 8 而不是 9,则序列 without lock 可以产生以下任何一个:8、9 或 17


更新:

基于一些 cmets,只是为了澄清一下术语。

当我说微操作时……它是用引号引起来的,所以我在这里创造了一个术语来进行讨论。它旨在直接转换为 x86 处理器文献中定义的 x86 微指令。我本可以[也许应该]说步骤

同样,虽然使用 x86 asm 表达这些步骤似乎最简单、最清晰,但我本可以更抽象:

(1) FETCH_MEM_TO_MREG _x
(2) ADD_TO_MREG 9
(3) STORE_MREG_TO_MEM _x

不幸的是,这些步骤纯粹是在硬件逻辑中执行的(即程序无法查看它们或使用调试器单步执行它们)。内存系统(例如高速缓存逻辑、DRAM 控制器等)将注意到(并且必须响应)步骤 (1) 和 (3)。 CPU 的 ALU 将执行第 (2) 步,这对内存逻辑是不可见的。

请注意,一些 RISC CPU 架构没有在内存上工作的添加指令,也没有锁定前缀。见下文。

除了阅读一些文献之外,检查效果的一种实用方法是创建一个使用多线程(通过pthreads)并使用一些 C 原子操作和/或pthread_mutex_lock 的 C 程序。

另外,这个页面Atomically increment two integers with CAS 有一个我给出的答案,还有一个链接到另一个人在 cppcon 给出的视频谈话(关于“无锁”实现)

在这个更通用的模型中,它还可以说明在没有正确锁定记录的数据库中会发生什么。

lock 如何实现的实际机制可能是特定于 x86 模型的。

并且,可能,目标指令特定(例如,lock 的工作方式不同,如果目标指令是 [say] addlxchg),因为处理器可能能够使用更高效/特殊类型的内存周期(例如,类似原子的“读-修改-写”)。

在其他情况下(例如,数据对于单个周期来说太宽或跨越缓存线边界),它可能必须锁定整个内存总线(例如,获取全局锁并强制完全序列化),进行多次读取,进行更改,进行多次写入,然后然后解锁内存总线。这种模式类似于将某些内容包装在互斥锁/解锁配对中的方式,仅在内存总线逻辑级别的硬件中完成

关于 ARM [a RISC cpu] 的说明。 ARM 仅支持ldr r1,memory_addressstr r1,memory_address,但不支持add r1,memory_address。它只允许add r1,r2,r3 [即它是“三元”] 或者可能是 add r1,r2,#immed。为了实现锁定,ARM 有两个特殊指令:ldrexstrex必须配对。在上面的抽象模型中,它看起来像:

ldrex r1,_x
add r1,r1,#9
strex r1,_x
// must be tested for success and loop back if failed ...

【讨论】:

有没有办法看到你提到的“微”操作,或者是操作码给出的?你的回答很有帮助,谢谢!后续,gcc,使用 O1 将 mov、add、mov 序列更改为上述指令。如果你说的是真的,那这真的是优化吗? 3 条 x86 指令和 1 条内存目标 x86 指令之间存在差异,后者在内部解码为读取、修改和写入操作。一方面,架构状态要么完成了添加,要么没有。中断不能中途停止序列,因此它是原子的在单核系统上。是的,这是一种优化:the memory operand can micro-fuse with the add to take fewer uops in the out-of-order pipeline. @SystemFun:例如,在 Intel SnB 系列 CPU 上,add m, r/i 在融合域中仅解码为 2 uop。总共有 4 个未融合的微指令,就像加载/添加/存储一样(存储地址/数据是分开的),但该序列将是 3 个融合域微指令。 (没有人设计过一个 x86 CPU,它融合了除分支之外的单独的 x86 指令。)除此之外,它显然需要更少的机器代码字节,这总是更好(所有其他条件都相同)。【参考方案2】:

没有。在处理器读取_x 的旧值和写回新值之间有一个很小的窗口;如果另一个 CPU 在那一刻写入_x,则该值将被覆盖。

在指令中添加LOCK前缀将使操作原子化。

【讨论】:

阅读内容到底在哪里?抱歉,我是组装新手。本质上对我来说,它看起来好像是在“就地”添加,这就是为什么我认为它可能是原子的。它是否将其加载到寄存器中以添加?是否有“就地”添加之类的东西? 这是暗示的。从内存中读取_x,将其加9,然后将结果写回。没有真正的“就地”操作。一切都必须交给 CPU 进行操作,即使它是在之后立即写回的。

以上是关于以下程序集是原子的,如果不是,为啥?的主要内容,如果未能解决你的问题,请参考以下文章

混合模式程序集是针对版本X构建的,如果没有其他配置信息,则无法在运行时的版本Y中加载

我的 C++ 程序集是如何从 C# 控制台应用程序而不是 ASP.NET 中看到的?

加载的程序集是 DEBUG 还是 RELEASE?

如果我已经创建了一个序列化程序集,为啥代码会编译一个序列化程序集?

为啥极少数情况下 bof/eof 之一对于新的非空记录集是正确的

混合模式程序集是针对版本“v1.1.4322”构建的