为什么在x86上使用memory_order_relaxed时要使用原子(带锁前缀)指令?

6
在 Visual C++ 2013 中,当我编译以下代码时:
#include <atomic>

int main()
{
    std::atomic<int> v(2);
    return v.fetch_add(1, std::memory_order_relaxed);
}

我在x86上得到以下汇编代码:

51               push        ecx  
B8 02 00 00 00   mov         eax,2 
8D 0C 24         lea         ecx,[esp] 
87 01            xchg        eax,dword ptr [ecx] 
B8 01 00 00 00   mov         eax,1 
F0 0F C1 01      lock xadd   dword ptr [ecx],eax 
59               pop         ecx  
C3               ret              

同样地,在x64上:

B8 02 00 00 00    mov         eax,2 
87 44 24 08       xchg        eax,dword ptr [rsp+8] 
B8 01 00 00 00    mov         eax,1 
F0 0F C1 44 24 08 lock xadd   dword ptr [rsp+8],eax 
C3                ret              

我不太理解:为什么对于一个int变量进行轻松的递增需要加lock前缀?
这是有原因的,还是因为没有考虑去掉它的优化?
*我使用了/O2/NoDefaultLib来减小文件大小并消除不必要的C运行时代码,但这与问题无关。

1
你期望什么?你认为在x86上实现放松内存顺序的原子增量应该如何实现? - Yakk - Adam Nevraumont
1
@Yakk: 呃,无论你怎么努力,我认为在x86上对齐的“int”执行非原子递增是不可能的...如果他们没有包括“lock”,会有什么问题? - user541686
1
@ZanLynx:好的,谢谢。请注意,在这里可移植性是无关紧要的,atomic 的实现本身必然是不可移植的。 - user541686
1
对齐无关紧要。即使对齐,一个简单的 inc [mem] 也不是原子操作。我们已经不再处于单核时代了。 - harold
1
@Mankarse:不,锁前缀是必须的,以将加载和存储与ALU操作结合成一个单一的原子RMW操作,而不是为了兼容386。编译器确实使用普通的mov加载来进行var.load(relaxed)(或seq_cst)操作。请参阅Can num ++ be atomic for 'int num'? - Peter Cordes
显示剩余15条评论
3个回答

7

即使使用 memory_order_relaxed ,仍需要锁才能确保原子性;增量/减量的要求太严格,不可能无锁。

试想一下没有锁的情况。

v = 0;

然后我们会生成100个线程,每个线程都使用这个命令:

v++;

然后,您等待所有线程完成后,您期望v的值是多少?不幸的是,它可能不是100。假设一个线程加载了v = 23的值,并且在创建24之前,另一个线程也加载了23,然后也写出了24。因此,线程实际上互相抵消了。这是因为增量本身不是原子性的。当然,加载、存储和添加本身可能是原子性的,但增量是多个步骤,因此不是原子性的。
但是使用std::atomic,所有操作都是原子性的,而不管std::memory_order设置如何。唯一的问题是它们将以什么顺序发生。memory_order_relaxed仍然保证原子性,只是可能与其附近发生的任何其他事情(甚至是对同一值进行操作)无序。

3
许多非x86 ISA可以进行原子读取-修改-写入操作,而不强制执行完整的内存屏障。例如,ARM和许多RISC ISA都具有加载链接/存储条件指令。除非使用屏障指令或ARM64加载获取指令,否则您将获得一个不会对其他内存操作排序的原子操作。因此,正如您所说,只有x86的怪癖是进行原子RMW操作的唯一方法也是完整的内存屏障 - Peter Cordes
刚刚意识到我从来没有在这里接受一个答案。对于任何在将近十年后阅读此内容的人,我认为当时我的困惑是因为xchg没有LOCK前缀也是原子操作,所以我认为其他类似的指令也是如此 - 但它们并不是。就是这样。 - user541686

1

原子操作,即使使用轻松的顺序,仍然必须是原子的。

即使当前CPU上的某些操作没有锁定前缀(提示:由于多核缓存,它们不是原子操作),也不能保证在未来的CPU上保持原子性。

如果您想通过依赖于汇编规范之外的特性(因此不能保证在未来的x86_64架构中得到保留)来优化二进制文件中的一个字节,并且使所有二进制文件在最新的架构上彻底失败,那将是短视的。

当然,在目前广泛使用的多核系统中,您实际上需要一个锁定前缀才能使其在当前CPU上运行。请参见Can num++ be atomic for 'int num'?


即使当前实现的CPU在没有锁前缀的情况下也可以进行原子更新,但是它们在没有lock的情况下不是原子性的,除非你谈论的是单处理器系统(单核心,单插槽)。请参见Can num++ be atomic for 'int num'?。这个答案听起来像是在1990年左右写的!(如果忽略多插槽SMP机器,则为2000年代初) - Peter Cordes
@PeterCordes,我的意思不是这个。关键是,“我们不要讨论该操作是否是原子操作的问题。你甚至可以提供证据,表明对于每个可用的CPU而言它都是原子操作。但这并不重要,因为你应该遵守规范,因为将来的CPU可能会打破这种假设。” - pqnet
我们几乎同时修正了措辞,我的稍晚一些并覆盖了你的。如果你喜欢你的措辞更好,请回滚我的编辑。要指出的一个有效观点是编译器必须尊重文档化的保证,而不仅仅是当前的行为。 (对于16字节对齐的原子加载/存储来说很不幸;在实践中,它在现代CPU上是原子性的,但供应商拒绝记录下来,因此atomic<__int128>必须使用lock cmpxchg16b进行纯负载和纯存储,而不是SSE。) - Peter Cordes
@PeterCordes,你说得对,似乎这意味着即使没有锁定,增量也是原子性的。我已经更改了措辞,使其更明确,避免让读者感到困惑。 - pqnet
1
@PeterCordes 在 stackoverflow 上的编辑也不是原子性的... - pqnet
1
它们是原子存储操作,而不是RMWs :P 我确实看到页面在“帖子已被编辑”栏弹出时移动了,就在我将鼠标移到“保存更改”时,所以我们实际上有顺序一致性;我还是点击了保存,打算检查一下我踩到的编辑内容。 - Peter Cordes

-1

首先,作为参考,请考虑一个普通的赋值。在Intel/64上生成以下内容:

// v = 10;
000000014000E0D0  mov         eax,0Ah  
000000014000E0D5  xchg        eax,dword ptr [v (014001BCDCh)]  

那么考虑一下放松的赋值:

// v.store(10, std::memory_order_relaxed);
000000014000E0D0  mov         dword ptr [v (014001BCDCh)],0Ah 

现在,std::atomic::fetch_add() 是一个读取-修改-写入操作,以“脏”方式执行这个操作没有多大意义。默认情况下,您将获得std::memory_order_seq_cst,如http://en.cppreference.com/w/cpp/atomic/atomic/fetch_add所述。因此,我认为,为此生成单个本地指令是有意义的。至少在Intel/64上是这样的。

// v.fetch_add(1, std::memory_order_relaxed)
000000014000E0D0  mov         eax,1  
000000014000E0D5  lock xadd   dword ptr [v (014001BCDCh)],eax  

毕竟,通过显式编写两个编译器将必须遵守的操作,您可以实现自己想要的功能:

// auto x = v.load(std::memory_order_relaxed);
000000014000E0D0  mov         eax,dword ptr [v (014001BCDCh)]  

// ++x;
000000014000E0D6  inc         eax  

//v.store(x, std::memory_order_relaxed);
000000014000E0D8  mov         dword ptr [v (014001BCDCh)],eax  

2
这完全没有抓住重点。即使使用mo_relaxed,原子fetch_add也保证了添加操作的原子性。由多个线程完成的1000次总增量将产生与v+=1000相同的效果。relaxed与acquire/release或seq_cst影响其他内存操作如何在原子操作周围重新排序。 - Peter Cordes

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