Arm 应该为互斥体获取交换循环(或队列获取加载循环)组合内存围栏还是应该避免?
假设重复采集操作,尝试加载或交换值,直到观察到的值为所需值 让我们以以下为起点:Arm 应该为互斥体获取交换循环(或队列获取加载循环)组合内存围栏还是应该避免?,arm,cpu-architecture,micro-optimization,memory-barriers,Arm,Cpu Architecture,Micro Optimization,Memory Barriers,假设重复采集操作,尝试加载或交换值,直到观察到的值为所需值 让我们以以下为起点: 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
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
}
}
我希望x86不会改变这一点
我想知道:
- 在存在差异的平台(ARM)上,此更改是否有好处或缺点
- 使用或不使用
指令的决定是否受到干扰yield
我不仅对
atomic\u flag::clear
/atomic\u flag::test\u和\u set
对感兴趣,我还对atomic::store
/atomic::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
}
}
void f(int n)
{
对于(int cnt=0;cnt<100;++cnt){
while(lock.test_和_set(std::memory_order_acquire))//获取锁
while(锁测试(标准::内存\u顺序\u松弛))
YieldProcessor();//自旋
std::cout暂停指令只是N条NOP指令的替代品,其中N条指令因处理器而异。此外,它对无序执行处理器中指令的重新排序也有影响。原子线程围栏是否比“暂停”更有效取决于典型的周期数自旋等待循环等待。原子线程围栏的执行延迟比暂停指令高。如果自旋等待周期比其他机制(如在x86平台上使用MONITOR-MWAIT指令对)大,则性能更好,也更节能。否则暂停就足够了。是的,避免捕获的一般想法是故障重试路径中的e屏障可能有用,但如果您只是在旋转,则故障情况下的性能几乎不相关。pause
或yield
节省电源。在x86上,pause
还提高了SMT友好性,避免了在修改另一个内核后离开循环时内存顺序错误推测正在旋转的内存位置
但这就是为什么CAS对成功和失败有单独的memory\u order
参数的原因。放松的失败只会让编译器上的障碍离开循环路径
atomic\u flag
test\u and\u set
没有此选项。手动执行此操作可能会损害像AArch64这样的ISA,因为它本可以执行获取RMW并避免显式围栏指令。(例如,使用ldarb
)
:带锁的原始循环。测试和设置(标准::内存\u顺序\u获取)
:
(是的,它只是用tst
而不是cbnz w1、.L6
测试低8位,这看起来像是错过了优化)
while(放松RMW)+std::原子线程围栏(std::内存顺序获取);
32位ARMv8的情况更糟,其中dmb ishld
不可用,或者编译器不使用它。您将获得一个dmb ish
完整屏障。
或者使用-march=armv8.1-a
vs
指令,例如pause或yield
,而我看到它只是对cpu的一个提示,没有yield
指令。如果你的意思是,它会等待一个中断,如果你的意思是,那么你就不应该使用它-sched_yield
是用于实时进程的,你的代码只会使用100%的cpu和调度程序blo检查其他进程。yield
是ARM指令。我的意思是。在x86上,这是一个没有任何问题的问题,所以我提到了ARM指令。我的意思是CPU指令,没有操作系统或运行时库函数。是的,这是一个关于做出优化提示的问题,所以pause
/yield
确实是“嗨”是的,避免故障重试路径内的获取障碍的一般想法可能是有用的,尽管如果您只是在旋转,故障情况下的性能几乎不相关。但这就是为什么CAS有单独的内存顺序成功和失败参数的原因。放松的故障只会让编译器停止运行ier在离开循环路径。atomic_flag
test_和_set
没有这个选项。是的,我是在问“仅仅旋转的性能”。我怀疑它背后有某种意义,否则就没有暂停
/产生
指令。感谢您提到了带有单独排序参数的CAS——我现在明白了为什么这些单独参数很有用。谢谢。我希望它也适用于存储
-释放
加载
-获取循环(相同方式,但不包括法律顾问/资深大律师)我假设,当分离的原子线程围栏更糟糕时,这种情况的存在是反对在一般实现中这样做的有力论据,因为它是对快速路径的悲观,而潜在优化是对waiting@AlexGuteniev:是的,没错。ARMv8是一个有趣的案例,其中反垄断比收购围栏便宜得多。作为收购操作的一部分,许多其他弱有序的ISA都会有一个同等的围栏。但是现在ARM是一个非常相关/重要的ISA,这是关注其特殊情况的好理由。@AlexGuteniev:对,只读旋转,直到它看起来有机会采取行动为止锁更可取。我应该提到这一点,但我认为,atomic_flag
是如此原始,它只是TAS和clear。但是的,当然也有一个测试。它确实是。atomic_flag::test
是C++20的加法。
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
}
}
# 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
.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
...
.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
...