是 x86-64 多核机器上 C++ Atomic 中 int 的读写
Are Reads and Writes of an int in C++ Atomic on x86-64 multi-core machine
我读过 this,我的问题很相似但又有些不同。
注意,我知道 C++0x 不能保证这一点,但我特别要求像 x86-64 这样的多核机器。
假设我们有 2 个线程(固定到 2 个物理内核)运行 以下代码:
// I know people may delcare volatile useless, but here I do NOT care memory reordering nor synchronization/
// I just want to suppress complier optimization of using register.
volatile int n;
void thread1() {
for (;;)
n = 0xABCD1234;
// NOTE, I know ++n is not atomic,
// but I do NOT care here.
// what I cares is whether n can be 0x00001234, i.e. in the middle of the update from core-1's cache lines to main memory,
// will core-2 see an incomplete value(like the first 2 bytes lost)?
++n;
}
}
void thread2() {
while (true) {
printf('%d', n);
}
}
线程 2 是否有可能看到 <b>n</b>
类似于 0x00001234,即在核心更新的中间1 到主内存的高速缓存行,核心 2 会看到不完整的值吗?
我知道一个 4 字节的 <b>int</b>
绝对适合典型的 128 字节长的缓存行,如果 <b>int</b>
确实存储在一个缓存行中,那么我相信这里没有问题......但是如果它跨越缓存行边界怎么办?即是否有可能某些 <strong>char</strong>
已经位于构成 <b> 的第一部分的缓存行内n</b>
在一个缓存行中,另一部分在下一行?如果是这样,那么 core-2 可能有机会看到一个不完整的值,对吧?
此外,我认为除非每个 <strong>char</strong>
或 <strong>short</strong>
或其他 <b> 小于 4 字节的 </b>
类型填充为 4 字节长,永远不能保证单个<b>int</b>
没有通过cache line边界,是吧?
如果是这样,那会建议通常即使设置单个 <strong>int</strong>
也不能保证在 x86-64 多系统上是原子的核心机?
我得到这个问题是因为当我研究这个主题时,不同帖子中的不同人似乎都同意,只要机器架构是正确的(例如 x86-64)设置 <b>int</b>
应该是原子的。但是正如我上面所说的那样不成立,对吧?
更新
我想介绍一下我的问题的背景。我正在处理一个实时系统,它正在对一些信号进行采样并将结果放入一个全局 int 中,这当然是在一个线程中完成的。在另一个线程中,我读取了这个值并对其进行了处理。
我不关心 set 和 get 的顺序,我只需要一个完整的(相对于损坏的整数值)值。
如果您正在寻找原子性保证,std::atomic<>
是您的朋友。不要依赖 volatile
限定符。
为什么这么担心?
取决于您的实施。如果 int
在您的平台上是原子的(在 x86-64 中,如果正确对齐,它们是原子的),std::atomic<int>
将减少到 int
。
如果我是你,我也会担心 int
溢出你的代码的可能性(这是未定义的行为)。
换句话说,std::atomic<unsigned>
是这里合适的类型。
x86 保证了这一点。 C++ 没有。如果你编写 x86 程序集,你会没事的。如果您编写 C++,则它是未定义的行为。由于您无法推断未定义的行为(毕竟它是未定义的),您必须降低并查看生成的汇编程序指令。如果他们按照您的意愿行事,那很好。但是请注意,当您更改编译器、编译器版本、编译器标志或任何可能更改优化器行为的代码时,编译器往往会更改生成的程序集,因此您将不得不不断检查汇编代码以确保它仍然正确。
更简单的方法是使用 std::atomic<int>
,这将保证生成正确的汇编程序指令,因此您不必经常检查。
另一个问题讲的是变量"properly aligned"。如果它穿过缓存行,则变量 not 正确对齐。例如,除非您特别要求编译器打包结构,否则 int
不会这样做。
您还假设使用 volatile int
优于 atomic<int>
。如果 volatile int
是在您的平台上同步变量的完美方式,那么库实现者肯定也会知道这一点并将 volatile x
存储在 atomic<x>
中。
没有要求 atomic<int>
必须特别慢,因为它是标准的。 :-)
这个问题几乎与 重复。那里的答案确实回答了你问的所有问题,但这个问题更侧重于 whether an int
(or other type?) whether the ABI / compiler question ,而不是当它发生时发生的事情。这个问题中还有其他内容也值得专门回答。
是的,它们几乎总是会在 int
适合单个寄存器的机器上(例如不是 AVR:8 位 RISC),因为编译器通常选择不使用多个存储指令,当他们可以使用 1.
正常的 x86 ABI 会将 int
对齐到 4B 边界,即使在结构内部也是如此(除非您使用 GNU C __attribute__((packed))
或其他方言的等效项)。但请注意 i386 System V ABI 仅将 double
对齐到 4 个字节;它只是外部结构,现代编译器可以超越它并赋予它自然对齐,.
但是你在 C++ 中合法做的任何事情都不能依赖于这个事实(因为根据定义它会涉及非 atomic
类型的数据竞争所以这是未定义的行为)。幸运的是,有一些有效的方法可以获得相同的结果(即大约相同的编译器生成的 asm,没有 mfence
指令或其他缓慢的东西)不会导致未定义的行为。
您应该使用 atomic
而不是 volatile
或者希望编译器不会优化非易失性 int
上的存储或加载,因为异步修改的假设是 volatile
和 atomic
重叠的方式之一。
I'm dealing with a real-time system, which is sampling some signal and putting the result into one global int, this is of course done in one thread. And in yet another thread I read this value and process it.
std::atomic
与 .store(val, std::memory_order_relaxed)
和 .load(std::memory_order_relaxed)
将为您提供您想要的。 HW-access 线程自由运行并将普通 x86 存储指令存储到共享变量中,而 reader 线程执行普通 x86 加载指令。
这是 C++11 表达你想要的方式,你应该期望它编译成与 volatile
相同的 asm . (如果你使用 clang,可能会有一些指令差异,但并不重要。)如果有任何 volatile int
没有足够对齐的情况,或任何其他极端情况, atomic<int>
将起作用(除非编译器错误)。除了可能在一个打包的结构中; IDK 如果编译器阻止你通过在结构中打包原子类型来破坏原子性。
理论上,您可能希望使用 volatile std::atomic<int>
来确保编译器不会将多个存储优化到同一个变量。参见 。但是现在,编译器不做那种优化。 (volatile std::atomic<int>
仍应编译为相同的轻量级 asm。)
I know a single 4-byte int definitely fits into a typically 128-byte-long cache line, and if that int does store inside one cache line then I believe no issues here...
自 PentiumIII 以来,所有主流 x86 CPUs 上的缓存行都是 64B;在此之前,32B 线是典型的。 (好吧 AMD Geode still uses 32B lines...) Pentium4 uses 64B lines, although it prefers to transfer them in pairs or something? Still, I think it's accurate to say that it really does use 64B lines, not 128B. This page 将其列为每行 64B。
据我所知,在任何级别的缓存中都没有使用 128B 行的 x86 微体系结构。
此外,只有英特尔 CPU 保证缓存的未对齐存储/加载在不跨越缓存行边界时是原子的。通常 x86 (AMD/Intel/other) 的基线原子性保证是不跨越 8 字节边界。请参阅 以获取 Intel/AMD 手册中的引述。
自然对齐几乎适用于最大保证原子宽度的任何 ISA(不仅仅是 x86)。
您问题中的代码需要非原子读取-修改写入,其中加载和存储分别是原子的,并且不对周围 loads/stores. 强加任何顺序
正如每个人所说,正确的方法是使用 atomic<int>
,但没有人确切指出 如何。如果你只是在 atomic_int n
上 n++
,你会得到(对于 x86-64)lock add [n], 1
,这将比你用 volatile
得到的要慢得多,因为它使整个 RMW 操作是原子的。 (也许这就是你回避 std::atomic<>
的原因?)
#include <atomic>
volatile int vcount;
std::atomic <int> acount;
static_assert(alignof(vcount) == sizeof(vcount), "under-aligned volatile counter");
void inc_volatile() {
while(1) vcount++;
}
void inc_separately_atomic() {
while(1) {
int t = acount.load(std::memory_order_relaxed);
t++;
acount.store(t, std::memory_order_relaxed);
}
}
来自 the Godbolt compiler explorer with gcc7.2 and clang5.0
的 asm 输出
毫不奇怪,对于 x86-32 和 x86-64,它们都编译为具有 gcc/clang 的等效 asm。 gcc 为两者制作相同的 asm,除了要递增的地址:
# x86-64 gcc -O3
inc_volatile:
.L2:
mov eax, DWORD PTR vcount[rip]
add eax, 1
mov DWORD PTR vcount[rip], eax
jmp .L2
inc_separately_atomic():
.L5:
mov eax, DWORD PTR acount[rip]
add eax, 1
mov DWORD PTR acount[rip], eax
jmp .L5
clang 优化得更好,使用
inc_separately_atomic():
.LBB1_1:
add dword ptr [rip + acount], 1
jmp .LBB1_1
注意缺少 lock
前缀,因此在 CPU 中解码为分离加载、ALU 添加和存储微指令。 (参见 )。
除了更小的代码大小外,其中一些微指令在来自同一指令时可以微融合,从而减少前端瓶颈。 (这里完全不相关;store/reload 的 5 或 6 个周期延迟的循环瓶颈。但如果用作更大循环的一部分,它将是相关的。)与寄存器操作数不同,add [mem], 1
比 Intel CPUs 上的 inc [mem]
更好,因为它的微熔丝更多:.
有趣的是,clang 使用效率较低的 inc dword ptr [rip + vcount]
作为 inc_volatile()
。
实际的原子 RMW 是如何编译的?
void inc_atomic_rmw() {
while(1) acount++;
}
# both gcc and clang do this:
.L7:
lock add DWORD PTR acount[rip], 1
jmp .L7
结构内部对齐:
#include <stdint.h>
struct foo {
int a;
volatile double vdouble;
};
// will fail with -m32, in the SysV ABI.
static_assert(alignof(foo) == sizeof(double), "under-aligned volatile counter");
但是atomic<double>
或atomic<unsigned long long>
会保证原子性。
对于 32 位机器上的 64 位整数 load/store,gcc 使用 SSE2 指令。不幸的是,其他一些编译器使用 lock cmpxchg8b
,这对于单独的存储或加载来说效率要低得多。 volatile long long
不会给你的。
volatile double
通常在正确对齐时对 load/store 是原子的,因为正常的方法已经是使用单个 8B load/store 指令。
我读过 this,我的问题很相似但又有些不同。
注意,我知道 C++0x 不能保证这一点,但我特别要求像 x86-64 这样的多核机器。
假设我们有 2 个线程(固定到 2 个物理内核)运行 以下代码:
// I know people may delcare volatile useless, but here I do NOT care memory reordering nor synchronization/
// I just want to suppress complier optimization of using register.
volatile int n;
void thread1() {
for (;;)
n = 0xABCD1234;
// NOTE, I know ++n is not atomic,
// but I do NOT care here.
// what I cares is whether n can be 0x00001234, i.e. in the middle of the update from core-1's cache lines to main memory,
// will core-2 see an incomplete value(like the first 2 bytes lost)?
++n;
}
}
void thread2() {
while (true) {
printf('%d', n);
}
}
线程 2 是否有可能看到 <b>n</b>
类似于 0x00001234,即在核心更新的中间1 到主内存的高速缓存行,核心 2 会看到不完整的值吗?
我知道一个 4 字节的 <b>int</b>
绝对适合典型的 128 字节长的缓存行,如果 <b>int</b>
确实存储在一个缓存行中,那么我相信这里没有问题......但是如果它跨越缓存行边界怎么办?即是否有可能某些 <strong>char</strong>
已经位于构成 <b> 的第一部分的缓存行内n</b>
在一个缓存行中,另一部分在下一行?如果是这样,那么 core-2 可能有机会看到一个不完整的值,对吧?
此外,我认为除非每个 <strong>char</strong>
或 <strong>short</strong>
或其他 <b> 小于 4 字节的 </b>
类型填充为 4 字节长,永远不能保证单个<b>int</b>
没有通过cache line边界,是吧?
如果是这样,那会建议通常即使设置单个 <strong>int</strong>
也不能保证在 x86-64 多系统上是原子的核心机?
我得到这个问题是因为当我研究这个主题时,不同帖子中的不同人似乎都同意,只要机器架构是正确的(例如 x86-64)设置 <b>int</b>
应该是原子的。但是正如我上面所说的那样不成立,对吧?
更新
我想介绍一下我的问题的背景。我正在处理一个实时系统,它正在对一些信号进行采样并将结果放入一个全局 int 中,这当然是在一个线程中完成的。在另一个线程中,我读取了这个值并对其进行了处理。 我不关心 set 和 get 的顺序,我只需要一个完整的(相对于损坏的整数值)值。
如果您正在寻找原子性保证,std::atomic<>
是您的朋友。不要依赖 volatile
限定符。
为什么这么担心?
取决于您的实施。如果 int
在您的平台上是原子的(在 x86-64 中,如果正确对齐,它们是原子的),std::atomic<int>
将减少到 int
。
如果我是你,我也会担心 int
溢出你的代码的可能性(这是未定义的行为)。
换句话说,std::atomic<unsigned>
是这里合适的类型。
x86 保证了这一点。 C++ 没有。如果你编写 x86 程序集,你会没事的。如果您编写 C++,则它是未定义的行为。由于您无法推断未定义的行为(毕竟它是未定义的),您必须降低并查看生成的汇编程序指令。如果他们按照您的意愿行事,那很好。但是请注意,当您更改编译器、编译器版本、编译器标志或任何可能更改优化器行为的代码时,编译器往往会更改生成的程序集,因此您将不得不不断检查汇编代码以确保它仍然正确。
更简单的方法是使用 std::atomic<int>
,这将保证生成正确的汇编程序指令,因此您不必经常检查。
另一个问题讲的是变量"properly aligned"。如果它穿过缓存行,则变量 not 正确对齐。例如,除非您特别要求编译器打包结构,否则 int
不会这样做。
您还假设使用 volatile int
优于 atomic<int>
。如果 volatile int
是在您的平台上同步变量的完美方式,那么库实现者肯定也会知道这一点并将 volatile x
存储在 atomic<x>
中。
没有要求 atomic<int>
必须特别慢,因为它是标准的。 :-)
这个问题几乎与 int
(or other type?) whether the ABI / compiler question ,而不是当它发生时发生的事情。这个问题中还有其他内容也值得专门回答。
是的,它们几乎总是会在 int
适合单个寄存器的机器上(例如不是 AVR:8 位 RISC),因为编译器通常选择不使用多个存储指令,当他们可以使用 1.
正常的 x86 ABI 会将 int
对齐到 4B 边界,即使在结构内部也是如此(除非您使用 GNU C __attribute__((packed))
或其他方言的等效项)。但请注意 i386 System V ABI 仅将 double
对齐到 4 个字节;它只是外部结构,现代编译器可以超越它并赋予它自然对齐,
但是你在 C++ 中合法做的任何事情都不能依赖于这个事实(因为根据定义它会涉及非 atomic
类型的数据竞争所以这是未定义的行为)。幸运的是,有一些有效的方法可以获得相同的结果(即大约相同的编译器生成的 asm,没有 mfence
指令或其他缓慢的东西)不会导致未定义的行为。
您应该使用 atomic
而不是 volatile
或者希望编译器不会优化非易失性 int
上的存储或加载,因为异步修改的假设是 volatile
和 atomic
重叠的方式之一。
I'm dealing with a real-time system, which is sampling some signal and putting the result into one global int, this is of course done in one thread. And in yet another thread I read this value and process it.
std::atomic
与 .store(val, std::memory_order_relaxed)
和 .load(std::memory_order_relaxed)
将为您提供您想要的。 HW-access 线程自由运行并将普通 x86 存储指令存储到共享变量中,而 reader 线程执行普通 x86 加载指令。
这是 C++11 表达你想要的方式,你应该期望它编译成与 volatile
相同的 asm . (如果你使用 clang,可能会有一些指令差异,但并不重要。)如果有任何 volatile int
没有足够对齐的情况,或任何其他极端情况, atomic<int>
将起作用(除非编译器错误)。除了可能在一个打包的结构中; IDK 如果编译器阻止你通过在结构中打包原子类型来破坏原子性。
理论上,您可能希望使用 volatile std::atomic<int>
来确保编译器不会将多个存储优化到同一个变量。参见 volatile std::atomic<int>
仍应编译为相同的轻量级 asm。)
I know a single 4-byte int definitely fits into a typically 128-byte-long cache line, and if that int does store inside one cache line then I believe no issues here...
自 PentiumIII 以来,所有主流 x86 CPUs 上的缓存行都是 64B;在此之前,32B 线是典型的。 (好吧 AMD Geode still uses 32B lines...) Pentium4 uses 64B lines, although it prefers to transfer them in pairs or something? Still, I think it's accurate to say that it really does use 64B lines, not 128B. This page 将其列为每行 64B。
据我所知,在任何级别的缓存中都没有使用 128B 行的 x86 微体系结构。
此外,只有英特尔 CPU 保证缓存的未对齐存储/加载在不跨越缓存行边界时是原子的。通常 x86 (AMD/Intel/other) 的基线原子性保证是不跨越 8 字节边界。请参阅
自然对齐几乎适用于最大保证原子宽度的任何 ISA(不仅仅是 x86)。
您问题中的代码需要非原子读取-修改写入,其中加载和存储分别是原子的,并且不对周围 loads/stores. 强加任何顺序
正如每个人所说,正确的方法是使用 atomic<int>
,但没有人确切指出 如何。如果你只是在 atomic_int n
上 n++
,你会得到(对于 x86-64)lock add [n], 1
,这将比你用 volatile
得到的要慢得多,因为它使整个 RMW 操作是原子的。 (也许这就是你回避 std::atomic<>
的原因?)
#include <atomic>
volatile int vcount;
std::atomic <int> acount;
static_assert(alignof(vcount) == sizeof(vcount), "under-aligned volatile counter");
void inc_volatile() {
while(1) vcount++;
}
void inc_separately_atomic() {
while(1) {
int t = acount.load(std::memory_order_relaxed);
t++;
acount.store(t, std::memory_order_relaxed);
}
}
来自 the Godbolt compiler explorer with gcc7.2 and clang5.0
的 asm 输出毫不奇怪,对于 x86-32 和 x86-64,它们都编译为具有 gcc/clang 的等效 asm。 gcc 为两者制作相同的 asm,除了要递增的地址:
# x86-64 gcc -O3
inc_volatile:
.L2:
mov eax, DWORD PTR vcount[rip]
add eax, 1
mov DWORD PTR vcount[rip], eax
jmp .L2
inc_separately_atomic():
.L5:
mov eax, DWORD PTR acount[rip]
add eax, 1
mov DWORD PTR acount[rip], eax
jmp .L5
clang 优化得更好,使用
inc_separately_atomic():
.LBB1_1:
add dword ptr [rip + acount], 1
jmp .LBB1_1
注意缺少 lock
前缀,因此在 CPU 中解码为分离加载、ALU 添加和存储微指令。 (参见
除了更小的代码大小外,其中一些微指令在来自同一指令时可以微融合,从而减少前端瓶颈。 (这里完全不相关;store/reload 的 5 或 6 个周期延迟的循环瓶颈。但如果用作更大循环的一部分,它将是相关的。)与寄存器操作数不同,add [mem], 1
比 Intel CPUs 上的 inc [mem]
更好,因为它的微熔丝更多:
有趣的是,clang 使用效率较低的 inc dword ptr [rip + vcount]
作为 inc_volatile()
。
实际的原子 RMW 是如何编译的?
void inc_atomic_rmw() {
while(1) acount++;
}
# both gcc and clang do this:
.L7:
lock add DWORD PTR acount[rip], 1
jmp .L7
结构内部对齐:
#include <stdint.h>
struct foo {
int a;
volatile double vdouble;
};
// will fail with -m32, in the SysV ABI.
static_assert(alignof(foo) == sizeof(double), "under-aligned volatile counter");
但是atomic<double>
或atomic<unsigned long long>
会保证原子性。
对于 32 位机器上的 64 位整数 load/store,gcc 使用 SSE2 指令。不幸的是,其他一些编译器使用 lock cmpxchg8b
,这对于单独的存储或加载来说效率要低得多。 volatile long long
不会给你的。
volatile double
通常在正确对齐时对 load/store 是原子的,因为正常的方法已经是使用单个 8B load/store 指令。