如果 8 字节由不同线程写入,是否可以保证在现代英特尔 x86 上读取 8 字节?

the*_*ang 4 c++ x86 multithreading atomic

struct Data {
    double a;
    double b;
    double c;
};
Run Code Online (Sandbox Code Playgroud)

如果在不同的线程上读取,但只有一个其他线程正在写入 a、b、c 中的每一个,那么读取每个双精度值是否正常?

如果我确保Data对齐,会出现什么情况?

struct Data {double a,b,c; } __attribute__((aligned(64));
Run Code Online (Sandbox Code Playgroud)

这将确保 a,b,c 中的每一个都对齐到 64,64+8, 64+16... 所以总是对齐到 8*8=64 位边界。

这个问题对原子 x86 指令的对齐要求及其答案让我认为Data::a/b/c从另一个线程写入并同时读取它们而不使用std::atomic.

是的,我知道std::atomic会解决这个问题,但这不是问题。

Pet*_*des 5

是的,从 P5 Pentium 开始,x86 ISA 保证对齐的 8 字节加载/存储是原子的。为什么在 x86 上自然对齐的变量原子上的整数赋值是原子的?

但这是 C++;不能保证 store 和 reloads 不会被优化掉。在一个线程中写入并在另一个线程中读取是 C++ 未定义行为;允许编译器假设它不会发生,打破天真的假设。这让他们可以在多次读取/写入时将 C++ 对象保存在寄存器中,最终只存储最终值。(包括某些指针指向的全局变量或内存。)

由于您还不知道volatileatomic<double>因此需要,最好阅读有关为您做的其他事情atomic<>,例如订购 wrt。除非您使用其他操作memory_order_relaxed(默认情况下seq_cst这会使商店变得昂贵,但在 x86 负载上仍然同样便宜)。并且(例如volatile)假设其他线程可能在此线程中的访问之间修改了对象。请参阅“int num”的 num++ 可以是原子的吗?,其中一些与 FP 加载和存储相关。

C++ 中的锁编程并不简单,除非您对同步/排序的需求为零。然后你“只是”必须确保你告诉编译器你的意思,用atomic<T>,或者作为一个 hack 用double.


由于 GCC 的std::atomic<double>withmo_relaxed不能有效编译,如果您只关心可移植性,您可能希望通过创建成员来推出自己的volatile。(甚至可以转换为(volatile double*)喜欢 Linux 内核的READ_ONCE/WRITE_ONCE宏)。使用 clang,您可以只使用atomic<double>memory_order_relaxed 并且事情将有效地编译。参见C++20 std::atomic<float>- std::atomic<double>.specializations例如你在 C++20 之前可以做什么;C++20 只添加原子 RMW add/sub fordouble所以你不必用 CAS 循环滚动你自己的。

volatile可能仍然会击败自动矢量化,但您当然可以使用_mm_load_pd或其他任何东西。(另请参阅x86_64 上的原子双浮点或 SSE/AVX 向量加载/存储- 请注意,即使对齐,SIMD 加载/存储也不一定是原子的。同样未记录的是它们是否是每个元素原子的,尽管我认为这可以安全地假设。 矢量加载/存储和收集/分散的每元素原子性?

什么时候在多线程中使用 volatile?通常永远不会,除非作为 GCC 的一种解决方法,它不会发出有效的 asm for atomic<double>,并且我们确切地知道如何volatile编译为 asm。


顺便说一句,您只需要alignas(8)确保成员是 8 字节对齐的。 将结构与整个缓存行对齐不会有什么坏处,除非它浪费空间。

对于性能:如果不同的线程在同一个缓存行中使用不同的变量,那就是“错误共享”并且对性能来说很糟糕。 不要将共享变量组合在一个结构中,除非它们通常作为一个组读取或写入。 否则,您肯定希望它们位于单独的 64 字节缓存行中。


请注意,a 上的数据竞争volatile仍然是 ISO C++ 未定义行为,但如果您使用的是 GNU C(根据您的 要求__attribute__),它的定义非常好。Linux 内核将它用于自己的手动原子(以及内联asm屏障),因此您可以假设它不会很快被有意取消支持。

TL:DR:在 GNU C 中,它或多或少地将volatile视为原子性的工作mo_relaxed,因为对齐的对象小到足以自然成为原子性。

  • @themagicalyang:不*完全*。如果线程 A 运行缓慢,每次迭代都会重新读取“double a”,以防它发生变化,这可能会命中缓存。线程 B 在与“a”同一行中存储“double b”将使所有其他核心中的该行无效,从而干扰线程 A,即使线程 A 从未读取“b”。 (4认同)