【问题标题】:Guaranteed Atomic Operations on POD types naturally aligned on Intel保证在英特尔自然对齐的 POD 类型上的原子操作
【发布时间】:2019-02-13 16:23:11
【问题描述】:

我有一个在 Intel Xeon 32 内核上运行的 C++ 多线程应用程序,使用 GCC 4.8.2 编译并启用了优化。

我有多个线程(例如 A、B、C)更新某些 POD 类型,另一个线程 D 每 K 秒读取这些变量并将其发送到 GUI。线程跨多个内核和套接字生成。写入受自旋锁保护。线程 A、B、C 对延迟敏感,高性能是一个关键方面。线程 D 对延迟不敏感。

类似:

Thread A,B,C
...
// a,b,c are up to 64 bits (let's say double)
spin-lock
a = computeValue();
b = computeValue();
c = computeValue();
spin-unlock
....

Thread D
...
// a,b,c are up to 64 bits (let's say double)
currValueA = a;
currValueB = b;
currValueC = c;
sendToGui(currValueA ,currValueB ,currValueC );
....

我想利用关于保证原子操作的第 8.1.1 段https://www.intel.com/content/www/us/en/architecture-and-technology/64-ia-32-architectures-software-developer-vol-3a-part-1-manual.html,并避免使用锁来保护线程 D 进行的读取。

我的理解是,如果 a、b、c 是自然对齐的(大小不超过 64 位),线程 D 没有风险可以读取在写入过程中中途获取的 a、b、c 值.换句话说,写入和读取将自动执行。线程 D 将读取旧值或新值。

我的理解正确吗?

我留给编译器 GCC 4.8.2 来处理对齐,即我不使用任何 gcc 内置指令或函数,如 std::alignas、sts::alignof 等。

我知道代码不可移植。 我宁愿不使用 std::atomic 以避免任何不必要的开销。

【问题讨论】:

  • 与std::atomic 并指定目标架构,编译器可能会自己进行优化。所以你的代码将是可移植的。让编译器工作给编译器。
  • 你有没有发现使用std::atomic造成的任何“不必要的开销”?
  • 将std::atomic 与memory_order_relaxed 一起使用对于纯加载和纯存储根本不会产生任何开销(并且即使在32 位代码中也能确保64 位值的正确对齐,其中@ 987654327@ 在 Linux 上),但它会干扰自动矢量化。 (当然,避免使用任何原子 RMW 操作)。如果您关心性能,请考虑使用更新的编译器,例如 gcc7 或 gcc8。自 4.8 以来进行了各种改进。
  • 感谢您的 cmets。我比较了使用原子存储(memory_relaxed 和 target 指定)和正常分配生成的程序集。他们看起来有点不同。我看到另外两条指令:movabsq 和 movq。我不能说它们是否对性能有任何影响。我也绝对应该尝试更新的 g++。谢谢。
  • 如果你想使用 Intel 规范进行“全金属”编程,你会使用内联汇编吗?

标签: c++ multithreading x86 atomic memory-alignment


【解决方案1】:

读取一个“在写入过程中取到一半”的值只是原子性的一个方面。

如今的处理器将值保存在特定于处理器的缓存中,因此在多处理器系统上,两个不同的处理器很可能对它们共享的 a 具有不同的值。将a 标记为原子可确保不同的处理器看到“相同”的值。

此外,编译器和处理器经常对计算进行重新排序,以便更好地利用处理设施。没关系,只要这些计算的结果没有改变。 (这就是 C++ 中的“好像”规则)。但是“未更改”是指在单个线程中执行。当多个线程冲击同一个对象时,在单个线程中工作的优化不一定有效。通常,您不希望您的单线程代码由不进行常见优化的偏执编译器编译,因为它们可能会破坏多线程代码。相反,将对象标记为原子表示编译器应该非常小心它移动的内容,因为该对象的值可以在幕后被其他代码更改。

所以你有一个选择:手动编写你的代码并希望你做对了,或者接受atomic库的作者可能对你的目标系统上的原子性了解更多,并且可能会这样做一份更好的工作。

【讨论】:

  • 感谢您的回答。如果我理解正确的重新排序,那么对于我的情况来说这不是一个真正的问题。如果延迟线程执行 a、b、c 分配的顺序与我写的不同,那没关系。如果 GUI 显示 b 和 c 已更新,而 a 没有,也可以。 Gui 不显示变量的最新值也是可以的。但我明白你的担忧。没有 atomic 这不是一个定义明确的行为。谢谢。
  • 您的第二段描述了正确的 C++ 症状,但导致问题的不是缓存。多 CPU 系统使用一致的缓存,因此存储到内存中的任何内容一旦提交到您自己的 L1d 缓存,其他内核就可以看到它。 (而std::atomic 默认为mo_seq_cst,这也阻止了StoreLoad 重新排序。)问题是数据竞争UB 允许编译器将非原子变量优化到寄存器中。例如while(!val){} 编译成 asm 就像 if(!val) infinite_loop(); MCU programming - C++ O2 optimization breaks while loop
  • @MCU -- SOME 多 CPU 系统使用一致的缓存。有些没有。
  • 你能举出一个真正的多核系统的例子吗发布商店?另请注意,这被标记为[intel](更正为[x86]),并且所有x86 系统都是缓存一致的。如果您使用 x86 CPU 构建了一个不连贯的类似 x86 的系统,那么您需要一个特殊的编译器,因为它不遵循 x86 内存模型。另见Is mov + mfence safe on NUMA?
  • C++11 内存模型假定缓存一致性,因为与获取加载同步的释放存储可以读取在发布商店。因此,如果需要显式刷新,所有可能属于 sychronizes-with 关系的内容都需要在 all 内存上进行显式刷新/同步。或者更可能的实现会添加函数来同步某些地址或地址范围,因为同步所有内容会破坏非连贯缓存的一半目的..
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-05-10
  • 2011-06-16
  • 2023-04-01
  • 2011-09-18
  • 2011-04-29
  • 2014-10-05
  • 2018-11-01
相关资源
最近更新 更多