假设一个重复的获取操作,尝试加载或交换一个值,直到观察到的值是期望值。
让我们来看看 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
}
}
现在让我们来考虑增强这种旋转。两个著名的是。
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系统不会有什么变化.
我想知道。
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
}
}
是的,在失败重试路径内避免获取障碍的一般想法可能是有用的,尽管在失败情况下的性能几乎没有关系,如果你只是在旋转。 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
...
暂停指令只是替代N条NOP指令的数量,其中N因处理器而异。此外,它还会对失序执行能力的处理器中指令的重新排序产生影响。atomic_thread_fence是否比 "pause "有一定的优势,取决于spin-wait循环等待的典型周期数是多少,atomic_thread_fence的执行延迟比pause指令高。如果自旋等待循环等待的周期数较多,那么其他机制,如在x86平台上使用MONITOR-MWAIT指令对,可以提供更好的性能,同时也很节能。否则pause指令就足够了。