【问题标题】:Is x86 CMPXCHG atomic, if so why does it need LOCK?x86 CMPXCHG 是原子的,如果是,为什么需要 LOCK?
【发布时间】:2015-03-06 10:13:56
【问题描述】:

Intel documentation

该指令可以与LOCK 前缀一起使用,以允许指令以原子方式执行。

我的问题是

  1. CMPXCHG 可以用内存地址操作吗?从文档中看似乎没有,但任何人都可以确认它只适用于寄存器中的实际值,而不是内存地址?

  2. 如果CMPXCHG不是原子的,并且必须通过LOCK CMPXCHG(带有LOCK前缀)实现高级语言级别的CAS,那么引入这样一条指令的目的到底是什么?

(我是从高级语言的角度来问的。也就是说,如果在x86平台上,无锁算法必须翻译成LOCK CMPXCHG,那么它仍然以LOCK为前缀。这意味着无锁算法是并不比那些精心编写的同步锁/互斥锁更好(至少在 x86 上)。这似乎也使裸 CMPXCHG 指令毫无意义,因为我猜引入它的主要目的是支持这种无锁操作。)

【问题讨论】:

  • 显然你可以使用内存地址,这就是重点。第一个操作数的类型是 r/m,所以你去吧。如果它本身不存在,你怎么能在指令前面加上lock
  • @harold 我不太明白什么不存在。如果您希望指令是原子的,请以 LOCK 为前缀。那么没有 LOCK 前缀的 CMPXCHG 是不是原子的?
  • 不,但在您的问题 2 中,您似乎在问为什么存在“没有锁的 cmpxchg”,这有点奇怪,因为如果没有这些部件,组合就无法存在 - 如果那不是你那么你能澄清一下吗?
  • @harold 我是从高级语言的角度来问的。 IE。如果无锁算法必须在x86平台上翻译成LOCK CMPXCHG,那么它仍然以LOCK为前缀。这意味着无锁算法并不比精心编写的同步锁/ murex(至少在 x86 上)更好。这似乎使 CMPXCHG 指令毫无意义,因为我想引入它的主要目的是支持这种无锁操作。
  • @Alex Suo:您将高级锁与恰好被命名为LOCK 的低级 CPU 功能混为一谈。无锁算法试图避免的高级锁将不得不将线程置于等待状态,直到锁可用,这是一项代价高昂的操作,与 CPU LOCK 前缀功能完全不同,后者可能会持有其他线程只有一条指令。

标签: concurrency x86 atomic lock-free compare-and-swap


【解决方案1】:

LOCK前缀是为当前命令锁定内存访问,使CPU管道中的其他命令不能同时访问内存。使用LOCK前缀,该命令的执行不会因为同时执行的其他命令的内存访问而被CPU管道中的另一个命令中断。 INTEL 手册说:

LOCK 前缀只能添加到以下指令中 并且仅适用于目的地的那些形式的指示 操作数是内存操作数:ADD、ADC、AND、BTC、BTR、BTS、CMPXCHG、 CMPXCH8B、CMPXCHG16B、DEC、INC、NEG、NOT、OR、SBB、SUB、XOR、XADD 和 XCHG.如果 LOCK 前缀与这些指令之一一起使用,并且 源操作数是内存操作数,未定义的操作码异常 (#UD) 可能会生成。

【讨论】:

  • 哪个管道?
  • 您的意思是“目前无法访问内存吗?”
  • 同样重要的是,lock 防止在加载和存储原子 RMW 之间出现来自 其他 内核(甚至 DMA 等)的访问。仅仅阻止来自该核心的其他操作交错是不够的。
【解决方案2】:

您将高级锁与恰好名为LOCK 的低级 CPU 功能混为一谈。

无锁算法试图避免的高级锁可以保护执行可能需要任意时间的任意代码片段,因此,这些锁将不得不将线程置于等待状态,直到锁可用,这是一项代价高昂的操作,例如意味着维护一个等待线程的队列。

这与 CPU LOCK 前缀功能完全不同,后者仅保护单个指令,因此可能仅在该单个指令的持续时间内保留其他线程。由于这是由 CPU 本身实现的,因此不需要额外的软件工作。

因此,开发无锁算法的挑战并不在于完全消除同步,它归结为将代码的关键部分减少为由 CPU 本身提供的单个原子操作。

【讨论】:

  • 那么说CMPXCHG仍然持有不同于程序级锁(即JVM锁)的锁是否正确
  • @Rohit Sachan:你可以说它持有 BUS 锁,但由于它适用于每次内存访问,唯一的区别是它适用于单个指令进行的两次内存访问,并且,更重要的是,在谈论“无锁编程”时只会让人感到困惑。换句话说,你应该始终关心讨论是关于硬件还是软件架构……
  • 我认为 OP 部分是在问“没有lockcmpxchg 有什么意义?”。请参阅my answer:英特尔这样设计它是因为它在单处理器系统上很有用。
  • @Peter Cordes:OP 的实际问题在this comment 中变得明显。在 OP 承认高级锁和指令前缀之间的这种混淆是实际问题之后,我才发布了一个答案。您的添加对于在搜索“锁定前缀”后发现此问题的未来读者可能仍然有价值。
  • 啊,我明白了。难怪这个问题没有引起太多关注。我只是在整理我的一些旧答案时在相关列表中注意到它,并没有涉足所有的cmets。
【解决方案3】:

看来你真正要问的是:

为什么 cmpxchglock 前缀不是隐含的,带有内存操作数 like it is for xchg(从 386 开始)?

简单的答案(其他人已经给出)是英特尔以这种方式设计的。但这引出了一个问题:

英特尔为什么要这样做? cmpxchg 有没有 lock 的用例?

在单 CPU 系统上,cmpxchg相对于其他线程或在同一 CPU 内核上运行的任何其他代码是原子的。 (但对于像内存映射 I/O 设备或对普通内存进行 DMA 读取的设备而言,“系统”观察者不适用,因此 lock cmpxchg 即使在单处理器 CPU 设计中也很重要)。

上下文切换只能发生在中断上,并且中断发生在指令之前或之后,而不是中间。在同一 CPU 上运行的任何代码都会将 cmpxchg 视为完全执行或根本不执行


例如,Linux 内核通常在编译时支持 SMP,因此它使用 lock cmpxchg 表示原子 CAS。但是当在单处理器系统上启动时,它会将lock 前缀修补到所有内联代码的nop,因为nop cmpxchg 运行速度比lock cmpxchg 快得多。有关详细信息,请参阅此LWN article about Linux's "SMP alternatives" system。它甚至可以在热插拔第二个 CPU 之前修补回 lock 前缀。

阅读更多关于单处理器系统上单指令原子性的信息in this answer,并在 @supercat's answer + comments 上的 num++ 可以为 int num 提供原子性。请参阅my answer there,了解有关原子性如何真正工作/如何实现读-修改-写指令的详细信息,例如lock cmpxchg


(同样的道理也适用于cmpxchg8b/cmpxchg16bxadd,它们通常只用于同步/原子操作,而不是为了让单线程代码运行得更快。当然是内存目标指令像add [mem], reg lock add [mem], reg 案例之外很有用。)

【讨论】:

  • " 但是当在单处理器系统上启动时,会将 lock 前缀修补到所有内联代码的 nop,因为 nop cmpxchg 的运行速度比 lock cmpxchg 快得多。"我假设您的意思是在单处理器系统中编译?因为我不知道操作系统可以在运行时修补此类编译指令。
  • @AlexSuo:不,Linux 的SMP alternatives 系统确实修补了 UP 系统上的内核映像。 (顺便说一句,如果它纯粹是编译时的事情,那将取决于您是否正在构建 for UP 系统,而不是 UP 系统。我想如果你省略了CONFIG_SMP,一些锁定/同步的东西可以完全省略,而不是在启动时被修补到NOP中。但现在可能没有那么多,尤其是标准CONFIG_PREEMPT允许内核代码预先已清空。)
  • @AlexSuo,恕我直言,这个答案应该被选为接受的答案。
  • @raiks:我也是这么想的; @ 987654327@ OP 的真正困惑与他们在问题本身中提出的不同,所以这就解释了为什么 Holger 写了那个答案并且 OP 接受了它,即使问题的有趣部分(IMO)是我回答的。
猜你喜欢
  • 2019-09-23
  • 2020-02-05
  • 2021-05-21
  • 2011-03-09
  • 1970-01-01
  • 2020-06-05
  • 2012-04-23
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多