atomic_flag是如何实现的?
How is atomic_flag implemented?
atomic_flag
是如何实现的?我觉得在 x86-64 上它相当于 atomic_bool
无论如何,但这只是一个猜测。 x86-64 实现可能与 arm 或 x86 有什么不同吗?
是的,在 atomic<bool>
和 atomic<int>
也是无锁的普通 CPU 上,它与 atomic<bool>
非常相似,使用相同的指令。 (x86 和 x86-64 具有相同的可用原子操作集。)
您可能认为它总是使用 x86 lock bts
或 lock btr
来设置/重置(清除)单个位,但做其他事情可能更有效(尤其是对于returns 一个 bool 而不是在其上分支的函数)。该对象是一个完整的字节,因此您可以只存储或交换整个字节。 (如果 ABI 保证该值始终为 0
或 1
,则您不必在将结果作为 bool
返回之前对其进行布尔化)
GCC 和 clang 将 test_and_set
编译为字节交换,并清除为 0
的字节存储。 我们得到(几乎)相同的 asm atomic_flag
test_and_set
作为 f.exchange(true);
#include <atomic>
bool TAS(std::atomic_flag &f) {
return f.test_and_set();
}
bool TAS_bool(std::atomic<bool> &f) {
return f.exchange(true);
}
void clear(std::atomic_flag &f) {
//f = 0; // deleted
f.clear();
}
void clear_relaxed(std::atomic_flag &f) {
f.clear(std::memory_order_relaxed);
}
void bool_clear(std::atomic<bool> &f) {
f = false; // deleted
}
On Godbolt 用于带有 gcc 和 clang 的 x86-64,以及用于 ARMv7 和 AArch64。
## GCC9.2 -O3 for x86-64
TAS(std::atomic_flag&):
mov eax, 1
xchg al, BYTE PTR [rdi]
ret
TAS_bool(std::atomic<bool>&):
mov eax, 1
xchg al, BYTE PTR [rdi]
test al, al
setne al # missed optimization, doesn't need to booleanize to 0/1
ret
clear(std::atomic_flag&):
mov BYTE PTR [rdi], 0
mfence # memory fence to drain store buffer before future loads
ret
clear_relaxed(std::atomic_flag&):
mov BYTE PTR [rdi], 0 # x86 stores are already mo_release, no barrier
ret
bool_clear(std::atomic<bool>&):
mov BYTE PTR [rdi], 0
mfence
ret
请注意,xchg
也是一种在 x86-64 上执行 seq_cst
存储的有效方法,通常比 gcc 使用的 mov
+ mfence
更有效. Clang 对所有这些都使用 xchg
(relaxed store 除外)。
有趣的是,clang 在 atomic_flag.test_and_set()
中的 xchg 之后重新布尔化为 0/1,但 GCC 在 atomic<bool>
之后执行它。 clang 在 TAS_bool 中做了一个奇怪的 and al,1
,这会将像 2
这样的值视为 false。这似乎毫无意义; ABI 保证内存中的 bool
总是存储为 0
或 1
字节。
对于 ARM,我们有 ldrexb
/ strexb
交换重试循环,或者只有 strb
+ dmb ish
用于纯存储。或者 AArch64 可以将 stlrb wzr, [x0]
用于 clear
或 assign-false 来执行(零寄存器的)顺序释放存储,而无需屏障。
在 most/sane 架构上,中断可能发生在执行硬件指令之后或之前。不是 "in between" 这是执行。所以指令 "happens"(即 "side effects")或者不发生。
例如,一个 16 位架构很可能具有硬件指令,可以通过一条指令对 16 位变量进行操作。因此递增 16 位变量将是一条指令。在 16 位变量中存储一个值将是一条指令。等等。 16 位变量不需要锁定,因为增量要么自动发生,要么不发生。在这种体系结构上不可能观察到 16 位变量增量的 "mid execution" 状态。这是一条指令。它不能被任何信号和中断打断"in between"。
16 位体系结构可能缺少在单个指令中递增 64 位变量的指令。它可能需要很多很多指令来对 64 位变量进行操作。所以对 std::atomic<uint64_t>
的操作需要编译器插入额外的同步指令来实现它的功能,实现与其他 std::atomic
变量的同步,等等
但是在这个架构上对 16 位变量的操作是单条指令,编译器不需要对它们做任何事情,指令执行后副作用将始终随处可见。
所以 atomic_flag
很可能只是一个变量,它具有特定处理器上的字的大小。这是为了使该处理器可以使用单个指令对该变量进行操作。实际上,这是一个 int
,但 int
不能保证对应于处理器的字长,并且访问 int
句柄不能保证是原子的。我相信通常 atomic_flag
与 sig_atomic_t
from posix (posix docs 相同)。额外的 atomic_flag
限制它的操作仅 bool
-ish 像:清除、设置和通知。
atomic_flag
是如何实现的?我觉得在 x86-64 上它相当于 atomic_bool
无论如何,但这只是一个猜测。 x86-64 实现可能与 arm 或 x86 有什么不同吗?
是的,在 atomic<bool>
和 atomic<int>
也是无锁的普通 CPU 上,它与 atomic<bool>
非常相似,使用相同的指令。 (x86 和 x86-64 具有相同的可用原子操作集。)
您可能认为它总是使用 x86 lock bts
或 lock btr
来设置/重置(清除)单个位,但做其他事情可能更有效(尤其是对于returns 一个 bool 而不是在其上分支的函数)。该对象是一个完整的字节,因此您可以只存储或交换整个字节。 (如果 ABI 保证该值始终为 0
或 1
,则您不必在将结果作为 bool
返回之前对其进行布尔化)
GCC 和 clang 将 test_and_set
编译为字节交换,并清除为 0
的字节存储。 我们得到(几乎)相同的 asm atomic_flag
test_and_set
作为 f.exchange(true);
#include <atomic>
bool TAS(std::atomic_flag &f) {
return f.test_and_set();
}
bool TAS_bool(std::atomic<bool> &f) {
return f.exchange(true);
}
void clear(std::atomic_flag &f) {
//f = 0; // deleted
f.clear();
}
void clear_relaxed(std::atomic_flag &f) {
f.clear(std::memory_order_relaxed);
}
void bool_clear(std::atomic<bool> &f) {
f = false; // deleted
}
On Godbolt 用于带有 gcc 和 clang 的 x86-64,以及用于 ARMv7 和 AArch64。
## GCC9.2 -O3 for x86-64
TAS(std::atomic_flag&):
mov eax, 1
xchg al, BYTE PTR [rdi]
ret
TAS_bool(std::atomic<bool>&):
mov eax, 1
xchg al, BYTE PTR [rdi]
test al, al
setne al # missed optimization, doesn't need to booleanize to 0/1
ret
clear(std::atomic_flag&):
mov BYTE PTR [rdi], 0
mfence # memory fence to drain store buffer before future loads
ret
clear_relaxed(std::atomic_flag&):
mov BYTE PTR [rdi], 0 # x86 stores are already mo_release, no barrier
ret
bool_clear(std::atomic<bool>&):
mov BYTE PTR [rdi], 0
mfence
ret
请注意,xchg
也是一种在 x86-64 上执行 seq_cst
存储的有效方法,通常比 gcc 使用的 mov
+ mfence
更有效. Clang 对所有这些都使用 xchg
(relaxed store 除外)。
有趣的是,clang 在 atomic_flag.test_and_set()
中的 xchg 之后重新布尔化为 0/1,但 GCC 在 atomic<bool>
之后执行它。 clang 在 TAS_bool 中做了一个奇怪的 and al,1
,这会将像 2
这样的值视为 false。这似乎毫无意义; ABI 保证内存中的 bool
总是存储为 0
或 1
字节。
对于 ARM,我们有 ldrexb
/ strexb
交换重试循环,或者只有 strb
+ dmb ish
用于纯存储。或者 AArch64 可以将 stlrb wzr, [x0]
用于 clear
或 assign-false 来执行(零寄存器的)顺序释放存储,而无需屏障。
在 most/sane 架构上,中断可能发生在执行硬件指令之后或之前。不是 "in between" 这是执行。所以指令 "happens"(即 "side effects")或者不发生。
例如,一个 16 位架构很可能具有硬件指令,可以通过一条指令对 16 位变量进行操作。因此递增 16 位变量将是一条指令。在 16 位变量中存储一个值将是一条指令。等等。 16 位变量不需要锁定,因为增量要么自动发生,要么不发生。在这种体系结构上不可能观察到 16 位变量增量的 "mid execution" 状态。这是一条指令。它不能被任何信号和中断打断"in between"。
16 位体系结构可能缺少在单个指令中递增 64 位变量的指令。它可能需要很多很多指令来对 64 位变量进行操作。所以对 std::atomic<uint64_t>
的操作需要编译器插入额外的同步指令来实现它的功能,实现与其他 std::atomic
变量的同步,等等
但是在这个架构上对 16 位变量的操作是单条指令,编译器不需要对它们做任何事情,指令执行后副作用将始终随处可见。
所以 atomic_flag
很可能只是一个变量,它具有特定处理器上的字的大小。这是为了使该处理器可以使用单个指令对该变量进行操作。实际上,这是一个 int
,但 int
不能保证对应于处理器的字长,并且访问 int
句柄不能保证是原子的。我相信通常 atomic_flag
与 sig_atomic_t
from posix (posix docs 相同)。额外的 atomic_flag
限制它的操作仅 bool
-ish 像:清除、设置和通知。