如果不同线程写入 8 字节,是否保证在现代英特尔 x86 上读取 8 字节是正常的?
Is a read of 8 bytes on modern intel x86 guaranteed to by sane if 8byte written by different thread?
struct Data {
double a;
double b;
double c;
};
如果在不同的线程上读取,但只有一个其他线程正在写入每个 a、b、c,每个 double 的读取是否正常?
如果我确保 Data
对齐会出现什么情况?
struct Data {double a,b,c; } __attribute__((aligned(64));
这将确保每个 a、b、c 都与 64,64+8、64+16...对齐,因此始终与 8*8=64 位边界对齐。
这个问题 alignment requirements for atomic x86 instructions 及其答案让我认为从另一个线程写入 Data::a/b/c
并在不使用 std::atomic
.[=17= 的情况下同时读取它们是完全有效的]
是的,我知道 std::atomic
可以解决这个问题,但这不是问题所在。
是的,从 P5 Pentium 开始,x86 ISA 保证对齐 8 字节 loads/stores 是原子的。
但这是C++;不能保证存储和重新加载不会被优化掉。在一个线程中写入并在另一个线程中读取是 C++ 未定义行为;允许编译器假设它不会发生,breaking naive assumptions。这让他们可以将 C++ 对象保存在多个 reads/writes 的寄存器中,最终只存储最终值。 (包括全局变量或某些指针指向的内存。)
由于您还不知道 volatile
或 atomic<double>
是出于这个原因,最好阅读 其他 [=12] =] 为你做,就像订购 wrt。其他操作,除非您使用 memory_order_relaxed
(默认值为 seq_cst
,这会使存储变得昂贵,但在 x86 上加载仍然同样便宜)。并且(如 volatile
)假设其他线程可能已经在该线程的访问之间修改了一个对象。请参阅 ,其中一些与 FP 加载和存储相关。
C++ 中的无锁编程 并不 简单,除非您对同步/排序的需求为零。然后你 "just" 必须确保你告诉编译器你的意思,用 atomic<T>
,或者用 double
.
作为 hack
由于 GCC 的 std::atomic<double>
和 mo_relaxed
编译效率不高,您 可能 想通过创建成员 volatile
来滚动自己的 volatile
如果你只关心便携性。 (甚至像 Linux 内核的 READ_ONCE
/ WRITE_ONCE
宏一样转换为 (volatile double*)
)。使用 clang,您只需将 atomic<double>
与 memory_order_relaxed 一起使用,即可高效编译。有关在 C++20 之前可以做什么的示例,请参阅 C++20 std::atomic<float>- std::atomic<double>.specializations; C++20 只为 double
添加了原子 RMW add/sub,所以你不必自己使用 CAS 循环。
volatile
可能仍然会打败自动矢量化,但您当然可以使用 _mm_load_pd
或其他任何东西。 (另见 - note that SIMD load/store aren't necessarily atomic even if aligned. Also undocumented is whether they're per-element atomic, although that is I think safe to assume. Per-element atomicity of vector load/store and gather/scatter?)
When to use volatile with multi threading? 通常从不,除非可能作为 GCC 的解决方法,它不会为 atomic<double>
发出有效的 asm,并且我们确切地知道 volatile
是如何编译为 asm.
顺便说一句,你只需要 alignas(8)
来确保成员是 8 字节对齐的。 将结构与整个缓存行对齐不会造成伤害,除非它浪费 space.
为了性能:如果不同的线程在同一缓存行中使用不同的变量,那是 "false sharing" 并且性能很糟糕。 不要将您的共享变量组合在一个结构中,除非它们通常作为一个组来读取或写入。否则您肯定希望它们位于单独的 64 字节缓存行中。
请注意 volatile
上的数据争用仍然是 ISO C++ 未定义行为,但如果您使用的是 GNU C(根据您的 __attribute__
的要求) ,它的定义非常明确。 Linux 内核将它 用于其自己的手动原子(连同内联 asm
用于屏障),因此您可以假设它不会很快有意不受支持。
TL:DR:在 GNU C 中,将 volatile
与 mo_relaxed
视为原子,对于小到足以自然成为原子的对齐对象,或多或少会做一些工作。
struct Data {
double a;
double b;
double c;
};
如果在不同的线程上读取,但只有一个其他线程正在写入每个 a、b、c,每个 double 的读取是否正常?
如果我确保 Data
对齐会出现什么情况?
struct Data {double a,b,c; } __attribute__((aligned(64));
这将确保每个 a、b、c 都与 64,64+8、64+16...对齐,因此始终与 8*8=64 位边界对齐。
这个问题 alignment requirements for atomic x86 instructions 及其答案让我认为从另一个线程写入 Data::a/b/c
并在不使用 std::atomic
.[=17= 的情况下同时读取它们是完全有效的]
是的,我知道 std::atomic
可以解决这个问题,但这不是问题所在。
是的,从 P5 Pentium 开始,x86 ISA 保证对齐 8 字节 loads/stores 是原子的。
但这是C++;不能保证存储和重新加载不会被优化掉。在一个线程中写入并在另一个线程中读取是 C++ 未定义行为;允许编译器假设它不会发生,breaking naive assumptions。这让他们可以将 C++ 对象保存在多个 reads/writes 的寄存器中,最终只存储最终值。 (包括全局变量或某些指针指向的内存。)
由于您还不知道 volatile
或 atomic<double>
是出于这个原因,最好阅读 其他 [=12] =] 为你做,就像订购 wrt。其他操作,除非您使用 memory_order_relaxed
(默认值为 seq_cst
,这会使存储变得昂贵,但在 x86 上加载仍然同样便宜)。并且(如 volatile
)假设其他线程可能已经在该线程的访问之间修改了一个对象。请参阅
C++ 中的无锁编程 并不 简单,除非您对同步/排序的需求为零。然后你 "just" 必须确保你告诉编译器你的意思,用 atomic<T>
,或者用 double
.
由于 GCC 的 std::atomic<double>
和 mo_relaxed
编译效率不高,您 可能 想通过创建成员 volatile
来滚动自己的 volatile
如果你只关心便携性。 (甚至像 Linux 内核的 READ_ONCE
/ WRITE_ONCE
宏一样转换为 (volatile double*)
)。使用 clang,您只需将 atomic<double>
与 memory_order_relaxed 一起使用,即可高效编译。有关在 C++20 之前可以做什么的示例,请参阅 C++20 std::atomic<float>- std::atomic<double>.specializations; C++20 只为 double
添加了原子 RMW add/sub,所以你不必自己使用 CAS 循环。
volatile
可能仍然会打败自动矢量化,但您当然可以使用 _mm_load_pd
或其他任何东西。 (另见
When to use volatile with multi threading? 通常从不,除非可能作为 GCC 的解决方法,它不会为 atomic<double>
发出有效的 asm,并且我们确切地知道 volatile
是如何编译为 asm.
顺便说一句,你只需要 alignas(8)
来确保成员是 8 字节对齐的。 将结构与整个缓存行对齐不会造成伤害,除非它浪费 space.
为了性能:如果不同的线程在同一缓存行中使用不同的变量,那是 "false sharing" 并且性能很糟糕。 不要将您的共享变量组合在一个结构中,除非它们通常作为一个组来读取或写入。否则您肯定希望它们位于单独的 64 字节缓存行中。
请注意 volatile
上的数据争用仍然是 ISO C++ 未定义行为,但如果您使用的是 GNU C(根据您的 __attribute__
的要求) ,它的定义非常明确。 Linux 内核将它 用于其自己的手动原子(连同内联 asm
用于屏障),因此您可以假设它不会很快有意不受支持。
TL:DR:在 GNU C 中,将 volatile
与 mo_relaxed
视为原子,对于小到足以自然成为原子的对齐对象,或多或少会做一些工作。