应该为互斥量获取交换循环(或队列获取加载循环)结合内存栅栏还是应该避免?
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
}
}
现在让我们考虑一下对这种旋转的改进。两个著名的是:
- 不要永远旋转,而是去OS等待某个时刻;
- 使用指令,例如
pause
或yield
,而不是空转。
我能想到第三个,我想知道它是否有意义。
我们可以使用 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
}
}
暂停指令只是 N 条 NOP 指令的替代,其中 N 因处理器而异。此外,它对能够乱序执行的处理器中的指令重新排序有影响。 atomic_thread_fence 是否会比 'pause' 提供一些好处取决于自旋等待循环等待的典型周期数是多少。 atomic_thread_fence 比暂停指令有更高的执行延迟。如果自旋等待周期比其他机制(如在 x86 平台上使用 MONITOR-MWAIT 指令对)大,则可以提供更好的性能并且也更节能。否则暂停就足够了。
是的,在失败重试路径中避免获取障碍的一般想法可能有用,尽管如果您只是在旋转,则失败情况下的性能几乎无关紧要。 pause
或 yield
省电。在 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
...
假设一个重复的获取操作,它尝试加载或交换一个值,直到观察到的值是所需的值。
我们以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
}
}
现在让我们考虑一下对这种旋转的改进。两个著名的是:
- 不要永远旋转,而是去OS等待某个时刻;
- 使用指令,例如
pause
或yield
,而不是空转。
我能想到第三个,我想知道它是否有意义。
我们可以使用 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
}
}
暂停指令只是 N 条 NOP 指令的替代,其中 N 因处理器而异。此外,它对能够乱序执行的处理器中的指令重新排序有影响。 atomic_thread_fence 是否会比 'pause' 提供一些好处取决于自旋等待循环等待的典型周期数是多少。 atomic_thread_fence 比暂停指令有更高的执行延迟。如果自旋等待周期比其他机制(如在 x86 平台上使用 MONITOR-MWAIT 指令对)大,则可以提供更好的性能并且也更节能。否则暂停就足够了。
是的,在失败重试路径中避免获取障碍的一般想法可能有用,尽管如果您只是在旋转,则失败情况下的性能几乎无关紧要。 pause
或 yield
省电。在 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
...