应该为互斥量获取交换循环(或队列获取加载循环)结合内存栅栏还是应该避免?

Should combining memory fence for mutex acquire-exchange loop (or queue acquire-load loop) be done or should it be avoided?

假设一个重复的获取操作,它尝试加载或交换一个值,直到观察到的值是所需的值。

我们以cppreference atomic flag example为起点:

void f(int n)
{
    for (int cnt = 0; cnt < 100; ++cnt) {
        while (lock.test_and_set(std::memory_order_acquire))  // acquire lock
             ; // spin
        std::cout << "Output from thread " << n << '\n';
        lock.clear(std::memory_order_release);               // release lock
    }
}

现在让我们考虑一下对这种旋转的改进。两个著名的是:

我能想到第三个,我想知道它是否有意义。 我们可以使用 std::atomic_thread_fence 获取语义:

void f(int n)
{
    for (int cnt = 0; cnt < 100; ++cnt) {
        while (lock.test_and_set(std::memory_order_relaxed))  // acquire lock
             ; // spin
        std::atomic_thread_fence(std::memory_order_acquire);  // acquire fence
        std::cout << "Output from thread " << n << '\n';
        lock.clear(std::memory_order_release);               // release lock
    }
}

我希望 x86 不会有任何变化。

我在想:


我不仅对atomic_flag::clear / atomic_flag::test_and_set 对感兴趣,我还对atomic<uint32_t>::store / atomic<uint32_t>::load 对感兴趣。


可能更改为松弛负载可能有意义:

void f(int n)
{
    for (int cnt = 0; cnt < 100; ++cnt) {
        while (lock.test_and_set(std::memory_order_acquire))  // acquire lock
             while (lock.test(std::memory_order_relaxed))
                 YieldProcessor(); // spin
        std::cout << "Output from thread " << n << '\n';
        lock.clear(std::memory_order_release);               // release lock
    }
}

暂停指令只是 N 条 NOP 指令的替代,其中 N 因处理器而异。此外,它对能够乱序执行的处理器中的指令重新排序有影响。 atomic_thread_fence 是否会比 'pause' 提供一些好处取决于自旋等待循环等待的典型周期数是多少。 atomic_thread_fence 比暂停指令有更高的执行延迟。如果自旋等待周期比其他机制(如在 x86 平台上使用 MONITOR-MWAIT 指令对)大,则可以提供更好的性能并且也更节能。否则暂停就足够了。

是的,在失败重试路径中避免获取障碍的一般想法可能有用,尽管如果您只是在旋转,则失败情况下的性能几乎无关紧要。 pauseyield 省电。在 x86 上,pause 还提高了 SMT 友好性,并避免在另一个内核修改了您正在旋转的内存位置后离开循环时内存顺序错误推测。

但这就是为什么 CAS 有单独的 memory_order 参数来表示成功和失败。宽松的失败可以让编译器只在离开循环路径上设置障碍。

但是,

atomic_flag test_and_set 没有该选项。 手动执行它可能会伤害像 AArch64 这样的 ISA,它可以完成获取 RMW 并避免显式的栅栏指令。 (例如 ldarb

Godbolt:原始循环 lock.test_and_set(std::memory_order_acquire):

# AArch64 gcc8.2 -O3
.L6:                            # do{
    ldaxrb  w0, [x19]           # acquire load-exclusive
    stxrb   w1, w20, [x19]      # relaxed store-exclusive
    cbnz    w1, .L6            # LL/SC failure retry
    tst     w0, 255
    bne     .L6             # }while(old value was != 0)
  ... no barrier after this

(是的,它只用 tst 而不是 cbnz w1, .L6 来测试低 8 位,这看起来像是错过了优化)

while(放松 RMW) + std::atomic_thread_fence(std::memory_order_acquire);

.L14:                          # do {
    ldxrb   w0, [x19]             # relaxed load-exclusive
    stxrb   w1, w20, [x19]        # relaxed store-exclusive
    cbnz    w1, .L14             # LL/SC retry
    tst     w0, 255
    bne     .L14               # }while(old value was != 0)
    dmb     ishld         #### Acquire fence
   ...

对于 32 位 ARMv8 更糟,其中 dmb ishld 不可用,或者编译器不使用它。 你会得到一个 dmb ish 完整的屏障。


-march=armv8.1-a

.L2:
    swpab   w20, w0, [x19]
    tst     w0, 255
    bne     .L2
    mov     x2, 19
  ...

对比

.L9:
    swpb    w20, w0, [x19]
    tst     w0, 255
    bne     .L9
    dmb     ishld                   # acquire barrier (load ordering)
    mov     x2, 19
...