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

时间:2014-06-15 22:45:38

标签: c++ visual-c++ x86 atomic relaxed-atomics

在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运行时代码,但这与问题无关。

3 个答案:

答案 0 :(得分:5)

因为它仍然需要锁定才能成为原子;即使使用memory_order_relaxed,增量/减量的要求也太严格,无法锁定。

想象没有锁的同样的东西。

v = 0;

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

v++;

然后你等待所有线程完成,你期望v是什么?不幸的是,它可能不是100.假设值v = 23由一个线程加载,并且在创建24之前,另一个线程也加载23然后也写出24。所以线程实际上互相否定。这是因为增量本身是非原子。当然,加载,存储,添加可能是原子的,但增量是多步,所以它不是原子的。

但是使用std :: atomic,无论std::memory_order设置如何,所有操作都是原子操作。唯一的问题是它们将发生什么样的顺序。memory_order_relaxed仍然保证原子性,它可能与其附近发生的任何事情有关,甚至可能在相同的值上运行。

答案 1 :(得分:1)

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

即使当前CPU上的某些操作是 原子,而没有lock前缀(提示:由于多核高速缓存,它们不能被使用),将来的CPU也无法保证

让所有二进制文件在最新架构上严重失败是短视的,只是因为您想依靠二进制代码中不属于汇编规范的功能来优化字节(因此不能保证保留)在未来的x86_64架构中)

当然,在这种情况下,多核系统非常普遍,因此您实际上需要使用lock前缀才能使其在当前CPU上工作。参见Can num++ be atomic for 'int num'?

答案 2 :(得分:-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()是一个Read-Modify-Write操作,以“脏”方式执行此操作毫无意义。默认情况下,您按http://en.cppreference.com/w/cpp/atomic/atomic/fetch_add获取std::memory_order_seq_cst。因此,我认为,为此生成单个本机指令是有意义的。至少在英特尔/ 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