【问题标题】:Does compare_exchange based loop benefit from pause?基于 compare_exchange 的循环是否受益于暂停?
【发布时间】:2018-11-07 00:37:35
【问题描述】:

在基于 CAS 的循环中,例如下面的循环,使用暂停对 x86 有好处吗?

void atomicLeftShift(atomic<int>& var, int shiftBy)
{
    While(true) {
        int oldVal = var;
        int newVal = oldVal << shiftBy;
         if(var.compare_exchange_weak(oldVal, newVal));
             break;
        else
            _mm_pause();
    }
}

【问题讨论】:

  • 看着docs,我会说是的。
  • 这应该没关系,因为失败的 CAS 应该很少见。如果这会有所作为,你可能有太多的争论

标签: c++ c++11 x86 atomic compare-and-swap


【解决方案1】:

不,我不这么认为。这不是旋转等待。它不是在等待另一个线程存储0 或其他东西。在lock cmpxchg 失败后立即重试确实有意义,而不是休眠约 100 个周期(在 Skylake 及更高版本上)或约 5 个周期(在早期的 Intel CPU 上)。

对于lock cmpxchg 完全完成(成功或失败)意味着缓存行现在在这个 核心上处于修改(或者可能只是独占?)状态,所以现在 是再试一次的最佳时机。

无锁原子的实际用例通常竞争激烈,否则您通常应该使用回退到操作系统辅助的睡眠/唤醒。

(但如果存在争用,locked 指令会进行硬件仲裁;在竞争激烈的情况下,我不知道核心是否有可能在输掉之前执行第二条 locked 指令再次缓存行。但希望是的。)

lock cmpxchg 不能虚假地失败,所以实际的活锁是不可能的:至少一个核心会通过让其 CAS 在这样的算法中成功而取得进展,因为每个核心都可以进行一轮。在 LL/SC 架构上,compare_exchange_weak 可能会虚假失败,因此到非 x86 的可移植性可能需要关心活锁,具体取决于实现细节,但我认为即便如此也不太可能。 (当然_mm_pause 仅适用于 x86。)


使用pause 的另一个原因是在离开旋转等待循环时避免内存顺序错误推测,该循环旋转只读等待在尝试原子声明之前看到锁已解锁。 (这比在 xchg 或 lock cmpxchg 上旋转并让所有等待的线程在缓存行上敲击要好。)

但这又不是问题,因为重试循环已经包含lock cmpxchg,这是一个完整的屏障以及原子 RMW,所以我认为这可以避免内存顺序错误推测。


特别是如果您有效/正确地编写循环以在重试时使用 cmpxchg 失败的加载结果,从循环中删除 var 的纯加载。

这是从 CAS 原语构造任意原子操作的规范方法。如果比较失败,compare_exchange_weak 会更新其第一个参数,因此您不需要在循环内再次加载。

#include <atomic>

int atomicLeftShift(std::atomic<int>& var, int shiftBy)
{
    int expected = var.load(std::memory_order_relaxed);
    int desired;
    do {
        desired = expected << shiftBy;
    } while( !var.compare_exchange_weak(expected, desired) );  // seq_cst
    return desired;
}

在 Godbolt 编译器资源管理器上使用 clang7.0 -O3 for x86-64 编译到这个 asm:

atomicLeftShift(std::atomic<int>&, int):      
    mov     ecx, esi
    mov     eax, dword ptr [rdi]        # pure load outside the loop

.LBB0_1:                              # do {
    mov     edx, eax
    shl     edx, cl                     # desired = expected << count
    lock            cmpxchg dword ptr [rdi], edx   # eax = implicit expected, updated on failure
    jne     .LBB0_1                   # } while(!CAS)
    mov     eax, edx                  # return value
    ret

重试循环中唯一的内存访问是lock cmpxchg,它不会受到内存顺序错误推测的影响。因此,不需要pause。


对于简单的退避延迟也不需要pause,除非您有很多争用并且想让一个线程连续对同一个共享变量执行多项操作以提高吞吐量。即在 cmpxchg 失败的罕见情况下让其他线程退出。

仅如果一个线程在同一变量的一行中执行多个原子操作是正常的(或者如果您有错误共享问题,则在同一缓存行中执行一个)是正常的,而不是将更多操作放入一次 CAS 重试中。

这在实际代码中可能很少见,但在合成微基准测试中很常见,在这种情况下,您让多个线程反复敲击共享变量,中间没有其他工作。

【讨论】:

  • 将负载移到循环外的好技巧!
  • 刚刚发现 Godbolt 也可以通过 llvm-mca 进行类似 IACA 的装配分析:godbolt.org/z/MPOE_H
  • 或比较:godbolt.org/z/5c6S5x。您如何看待它的输出?
  • @Trass3r:很好,我不知道 llvm 有这个。与此不同,查看 IACA 有用的循环会很有趣。当一个循环在运行它的核心内部的微架构事物上出现瓶颈时,IACA / LLVM-MCA 很有用,而不是在内存上。 pause 的版本在快速路径上有一个分支,就像我在答案中所说的那样,基本上没有优势,除非在一个奇怪的情况下,如果你的 CAS 失败,你想让另一个线程继续搞乱缓存行,即使尽管 CAS 完全执行意味着您现在拥有处于独占状态(或可能已修改)的缓存行。
  • 是的,我不知道我是怎么错过的:lists.llvm.org/pipermail/llvm-dev/2018-March/121490.html
猜你喜欢
  • 2012-06-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-05-09
相关资源
最近更新 更多