事实证明所有(?)编译器都将std::atomic::load(std::memory_order_relaxed)
视为易失性负载(通过
__iso_volatile_load64
等)。他们根本不优化或重新排序。即使丢弃加载的值仍然会生成加载指令,因为编译器将其视为可能产生副作用。
p
指向共享内存中单调递增的 8 字节计数器,该计数器仅写入到外部我的进程。我的程序只从这个地址读取。 我想以这样的方式读取这个计数器:
原子(无撕裂)
此柜台保留订单(因此x = *p; y = *p;
意味着
x <= y
)
不被视为不透明/优化障碍(上面#2除外)
(void)*p;
)被丢弃,other指令被自由地重新排序这个内存访问等等 除了使用易失性负载之外,还有什么方法可以在 MSVC 或 Clang 上实现此目的吗?
(特定于实现的黑客/内在函数/等。
都可以,只要这些特定的实现永远不会将其视为未定义的行为,因此不存在错误代码生成的风险。)
std::atomic<uint64_t> *p
或
std::atomic_ref<>
与
std::memory_order_relaxed
可以满足您的大部分需求,除了公共子表达式消除 (CSE) 之外。在未来的编译器中,您甚至可能会得到有限的负载,或者至少优化掉未使用的负载。纸面上的 ISO C++ 保证勉强足以满足您的用例。
我不知道有什么比这更弱但仍然安全的东西。让它变得简单(非原子/非易失性)并且不会给你读读连贯性。即使您在源代码中写入int x = *p;
,某些(也许不是全部)后来使用的
x
实际上可能会从
*p
重新加载。请参阅 LWN 上的谁害怕一个糟糕的优化编译器? 的“发明的负载”部分。稍后使用
x
时可能会发生这种情况,但并非全部,从而使变量值发生变化。或者对于
x
但不是
y
,允许违反
x<=y
。也许您使用 GNU C 内联汇编(如
int x = *p; asm("" ::: "memory");
)来告诉编译器
*p
可能已更改。或者可能是像
asm("" : "+g"(*p))
这样对优化伤害较小的东西,告诉它只忘记
*p
的值,而不成为所有内存重新排序的编译器障碍。但这仍然会阻止多次加载的 CSE,因为您仍然手动告诉编译器在哪里忘记事情。此外,假设它不是
x = *p
或
volatile
,则可能会非原子地执行
atomic
,具体取决于周围的代码;64 位计算机上的哪些类型在 gnu C 和 gnu C++ 中自然是原子的? -- 意味着它们具有原子读取和原子写入 显示了 AArch64 上的 64 位存储示例,GCC 选择使用对两半具有相同值的
stp
进行编译,这不能保证原子性直到 ARMv8.4 之类的。因此,使用非原子类型并依赖内存屏障是两个世界中最糟糕的,并且“不能”通过任何特定于编译器的保证来保证工作;它仍然是 MSVC 和 GNU C++ 中的数据竞赛 UB
std::atomic<>
relaxed
满足您的正确性要求
std::atomic
atomic<uint64_t>
在所有主流 x86 编译器上应该是无锁的,即使在 32 位模式下也是如此。 (对 MSVC 不是 100% 确定,但 GCC 和 Clang 知道如何使用 SSE2
movq
在 32 位模式下进行 8 字节原子加载。)甚至
relaxed
[intro.races]/16
):后续读取将在修改顺序中看到相同或较晚的值。这可以防止编译时重新排序,即使在非 x86 ISA 上,一致的缓存 + 硬件保证也可以免费实现这一点(无需任何额外的屏障指令)。编译器可以并且确实围绕原子加载/存储对其他变量上的其他内存操作进行重新排序。
relaxed
也是如此(以及带有
volatile
的 MSVC,以确保即使在针对 x86 进行编译时,也不会将其视为 /volatile:iso
/
acquire
)。但是
release
和
std::atomic
可移植地准确表达您想要的语义,因此未来的编译器可能会更好地优化。并且
relaxed
操作无法在编译时与其他
volatile
操作一起重新排序,无论位于哪个位置,这与原子
volatile
不同,它可以。