【问题标题】:Why does memory_order_relaxed use atomic (lock-prefixed) instructions on x86?为什么 memory_order_relaxed 在 x86 上使用原子(锁定前缀)指令?
【发布时间】:2014-06-15 22:45:38
【问题描述】:

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

【问题讨论】:

  • 你期待什么?您认为应该如何在 x86 上实现轻松的内存顺序原子增量??
  • @Yakk:嗯,我认为无论你怎么努力,都无法在 x86 上对齐的 int 上执行非原子增量......如果他们只是不包括lock
  • @ZanLynx:好的,谢谢。请注意,此处的可移植性无关紧要,atomic 的实现本身必然是不可移植的。
  • 对齐无关紧要。一个普通的旧 inc [mem] 即使在对齐时也不是原子的。我们不再处于单核时代。
  • @Mankarse:不,需要锁定前缀才能将加载和存储与 ALU 操作一起加入单个原子 RMW 操作,而不是 386 兼容。编译器确实使用普通的 mov 加载 var.load(relaxed) (或 seq_cst )。见Can num++ be atomic for 'int num'?

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


【解决方案1】:

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

想象一下没有锁的情况。

v = 0;

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

v++;

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

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

【讨论】:

  • 许多非 x86 ISA 可以在不强制使用完整内存屏障的情况下进行原子读取-修改-写入。例如ARM 与许多 RISC ISA 一样,具有加载链接/存储条件。除非您使用屏障指令或 ARM64 加载获取,否则您将获得一个不会对其他内存操作进行排序的原子操作。正如您所说,执行原子 RMW 操作的唯一方法也是一个完整的内存屏障,这只是 x86 的一个怪癖
【解决方案2】:

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

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

仅仅因为你想优化你的二进制文件中的一个字节,依赖于一个不属于汇编规范的特性(因此不能保证保留在未来的 x86_64 架构中)

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

【讨论】:

  • 即使当前实现的 CPU 会在没有锁定前缀的情况下进行原子更新 - 但如果没有 lock,它们就不是原子的,除非您在谈论单处理器系统。 (单芯,单插座)。请参阅Can num++ be atomic for 'int num'? 这个答案听起来像是写于 1990 年之类的! (如果您忽略多插槽 SMP 机器,则为 2000 年代初期)
  • @PeterCordes 我不是这个意思。重点是,“我们不要讨论那个操作是否是原子的。你甚至可以证明每个可用的 CPU 都是原子的。没关系,因为你应该遵守规范,因为未来的 CPU 可能会崩溃那个假设”
  • 我们几乎同时修正了措辞;我的稍晚一点,覆盖了你的。如果您更喜欢您的措辞,请回滚我的编辑。编译器必须尊重 documented 保证,而不仅仅是当前行为,这是一个有效的观点。 (不幸的是 16 字节对齐的原子加载/存储;实际上它在现代 CPU 上是原子的,但供应商拒绝记录,因此 atomic&lt;__int128&gt; 必须使用 lock cmpxchg16b 进行纯加载和纯存储,而不是 SSE。)跨度>
  • @PeterCordes 在 stackoverflow 上的编辑也不是原子的...
  • 它们是原子存储操作,而不是 RMW :P 当我将鼠标移到“保存更改”时,弹出“帖子已编辑”栏时,我确实看到了页面转移,所以我们确实有顺序一致性;无论如何,我只是点击了保存,以查看我踩到了什么编辑。
【解决方案3】:

首先,作为参考,考虑一个正常的分配。它在 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() 是一个读取-修改-写入操作,以“肮脏”的方式执行此操作几乎没有意义。默认情况下,您将按照http://en.cppreference.com/w/cpp/atomic/atomic/fetch_add 获得std::memory_order_seq_cst。所以,我认为,为此生成一条本机指令是有意义的。至少在便宜的 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  

【讨论】:

  • 这完全没有抓住重点。即使使用mo_relaxed,原子 fetch_add 也保证添加是原子完成的。由多个线程完成的 1000 次增量将具有与v+=1000 相同的效果。放松与获取/释放或 seq_cst 会影响 其他 内存操作如何围绕原子操作重新排序。
猜你喜欢
  • 1970-01-01
  • 2016-05-24
  • 1970-01-01
  • 2012-04-23
  • 2013-05-20
  • 1970-01-01
  • 2019-09-23
  • 2014-04-30
  • 1970-01-01
相关资源
最近更新 更多