c++ - 基于 compare_exchange 的循环是否受益于暂停?

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

在基于 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();
    }
}

最佳答案

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

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

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

(但如果存在争用,则会对锁定指令进行硬件仲裁;在竞争激烈的情况下,我不知道内核是否有可能执行第二个在再次丢失缓存行之前锁定指令。但希望是。)

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


使用 pause 的另一个原因是在离开自旋等待循环时避免内存顺序错误推测,该自旋等待循环以只读方式旋转以等待看到锁已解锁,然后再尝试以原子方式声明它。 (这比在 xchglock cmpxchg 上旋转并让所有等待的线程都在高速缓存行上敲打要好。)

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


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

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

#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;
}

clang7.0 -O3 for x86-64 编译对于这个 asm,在 Godbolt 编译器资源管理器上:

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 重试中。

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

关于c++ - 基于 compare_exchange 的循环是否受益于暂停?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53182137/

相关文章:

c++ - 存储一组非重叠范围并严格查找值是否存在于任何一个范围中

c++ - c 和 c++ 中 sizeof 运算符的工作是否不同

c++ - 我应该如何从 Eigen 3中的张量切片中获取 vector ?

hyperlink - x86 masm Hello World

assembly - 为什么 Assembly x86_64 系统调用参数不像 i386 那样按字母顺序排列

assembly - 如何在不支持硬件的情况下测试 AVX-512 指令?

c++ - 检索整数变量的值

C++ 如何从虚拟类的模板化子类中获取数据?

C++ 方法线程

c++ - std::transform Vector For Euclidean 距离