对于mutex获取-交换循环(或队列获取-加载循环),是否应该结合内存栅栏,还是应该避免?

问题描述 投票:1回答:1

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

让我们来看看 cppreference原子标志示例 作为出发点。

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

现在让我们来考虑增强这种旋转。两个著名的是。

  • 不要永远旋转,而是在某一时刻转到OS等待。
  • 使用一个指令,比如 pauseyield 而不是不合作的旋转。

我可以想到第三个,我想知道它是否有意义.我们可以用一个新的方法来解决这个问题。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系统不会有什么变化.

我想知道。

  • 在有差异的平台(ARM)上,这种改变有什么好处或坏处吗?
  • 是否会对决定使用或不使用 yield 指导?

我不仅对 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
    }
}
arm cpu-architecture micro-optimization memory-barriers
1个回答
1
投票

是的,在失败重试路径内避免获取障碍的一般想法可能是有用的,尽管在失败情况下的性能几乎没有关系,如果你只是在旋转。 pause 或者 yield 省电。 在x86上。pause 也提高了SMT的友好度,并且避免了在另一个核心修改了你正在旋转的内存位置后离开循环时的内存顺序误判。

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

atomic_flag test_and_set 并没有这个选项,但。 手动做有可能会伤害到像AArch64这样的ISA,它们可以做一个acquisition 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

(是的,它看起来像是一个遗漏的优化,因为它只测试了低8位与 tst 而不是仅仅 cbnz w1, .L6)

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
  ...

vs. 暂停指令

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

1
投票

暂停指令只是替代N条NOP指令的数量,其中N因处理器而异。此外,它还会对失序执行能力的处理器中指令的重新排序产生影响。atomic_thread_fence是否比 "pause "有一定的优势,取决于spin-wait循环等待的典型周期数是多少,atomic_thread_fence的执行延迟比pause指令高。如果自旋等待循环等待的周期数较多,那么其他机制,如在x86平台上使用MONITOR-MWAIT指令对,可以提供更好的性能,同时也很节能。否则pause指令就足够了。

© www.soinside.com 2019 - 2024. All rights reserved.