让我们考虑这个简单的代码:
#include <atomic>
std::atomic<int> a;
void f(){
for(int k=0;k<100;++k)
a.load(std::memory_order_relaxed);
}
MSVC、Clang 和 GCC 都执行 100 次 a 负载,虽然看起来很明显它可以被优化掉。我期望函数
f
是一个 nop (请参阅生成的代码 here)
实际上,我期望为易失性原子生成代码:
volatile std::atomic<int> va;
void g(){
for(int k=0;k<100;++k)
va.load(std::memory_order_relaxed);
}
为什么编译器不优化掉不必要的原子加载?
如果调用者这样做
g(); atomic_thread_fence(acquire);
,它可能会与另一个线程创建一个发生之前的关系,如果执行了零负载,则该关系将不存在。由于加载结果被丢弃,它现在可以知道它同步的什么,但完全优化掉负载的效果并不完全明显。
也许存在一些计时原因,在某些实际实现上意味着另一个线程中的某些存储应该对该宽松的负载可见。这听起来相当手动,但某些运行时计时条件可能会导致无 UB 执行,而这种执行可能会通过优化所有负载而被破坏。
将 100 个负载压缩为 1 而不是 0 不会出现此问题,但仍然需要特定的优化过程来查找未使用的负载,这些负载未被任何内存排序效果(如栅栏)分隔。似乎很难否认它的安全性。如果代码依赖于每个负载发生作为延迟措施(或用于 MMIO),则应使用 volatile atomic
。
编译器是否可以优化两个原子加载? / 为什么编译器不合并冗余的 std::atomic 写入?
这些都不是将 100 个未使用的负载压缩到 1 个的真正障碍,但它与编译器现在选择根本不优化原子有关,因为它与更棘手的问题相关。
实际原因
volatile
的支持来处理原子,即不假设多次读取会给出相同的值。这有一个副作用,基本上就像对待他们一样
volatile atomic
。对于 GCC,o11c 链接了 https://gcc.gnu.org/wiki/Atomic/GCCMM/Optimizations,了解 GCC 在优化原子方面的限制。它声称 [intro.races]/19 将禁止将 int x=a.load(relaxed); int y=a.load(relaxed);
视为
x=y=a.load(relaxed);
。但这是一种误读。它禁止以 other顺序执行它们,这可能导致
x
从 a
的修改顺序中具有更新的值,但强制它们都具有相同的修改顺序值并不违反读读连贯规则[intro.races]/16注释是总结。该 GCC wiki 页面的最后一次编辑显然是在 2016 年,即 WG21/P0062R1 之前;希望大多数涉及 C/C++ 原子的 GCC 开发人员从那时起就意识到 ISO 标准在纸面上允许优化,即使对于所使用的负载也是如此。 此外,编译器开发人员可能不愿意添加寻找几乎无法盈利的优化的代码。 GCC、LLVM 和 MSVC 等编译器的较大代码库需要更多的开发工作来维护,可能会减慢其他功能的添加和维护速度。
此外,寻找这样的优化会使编译时间变慢。在这里这可能不是问题;现代提前编译器已经将程序逻辑转换为 SSA 形式,即使在比这更简单的情况下,也应该很容易找到未使用的结果(例如,将加载结果分配给优化后未使用的本地变量时)。对于这种微不足道的情况,编译器已经可以警告未使用的返回值。