Meh*_*dad 5 c++ x86 atomic clang visual-c++
事实证明,所有(?)编译器都将其视为std::atomic::load(std::memory_order_relaxed)易失性负载(通过__iso_volatile_load64等)。
他们根本不优化或重新排序。即使丢弃加载的值仍然会生成加载指令,因为编译器将其视为可能产生副作用。
因此,松弛载荷不是最佳的。照这样说...
假设p指向共享内存中一个单调递增的 8 字节计数器,该计数器仅写入我的进程之外。我的程序只从这个地址读取。
我想以这样的方式读取这个计数器:
负载是原子的(无撕裂)
该计数器保留顺序(因此这x = *p; y = *p;意味着x <= y)
负载不被视为不透明/优化障碍(除了上面的#2)
特别是,这里的目的是编译器在正常内存访问时执行尽可能多的优化,例如:无用的加载(如(void)*p;)被丢弃,其他指令围绕此内存访问自由重新排序,等等。
除了使用易失性负载之外,还有什么方法可以在 MSVC 或 Clang 上实现此目的吗?
(特定于实现的黑客/内在函数/等都可以,只要这些特定的实现永远不会将其视为未定义的行为,因此不存在错误代码生成的风险。)
const std::atomic<uint64_t> *p或者std::atomic_ref<>withstd::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以非原子方式执行,具体取决于周围的代码;64 位计算机上的哪些类型在 gnu C 和 gnu C++ 中自然是原子的?-- 意味着它们具有原子读取和原子写入,显示了 AArch64 上的 64 位存储示例,GCC 选择使用相同值对这对的两半进行编译,直到 ARMv8.4 或某物。因此,使用非原子类型并依赖内存屏障是两全其美的,并且不能通过任何特定于编译器的保证来保证其工作;它仍然是 MSVC 和 GNU C++ 中的数据竞争 UBvolatileatomicstp
std::atomic<>满足relaxed您的正确性要求无锁std::atomic加载始终会编译为硬件中原子的内容,并且atomic<uint64_t>即使在 32 位模式下,在所有主流 x86 编译器上也应该是无锁的。(对 MSVC 不是 100% 确定,但 GCC 和 Clang 知道如何movq在 32 位模式下使用 SSE2 进行 8 字节原子加载。)
即使relaxed原子也具有读读一致性([intro.races]/16):后续读取将在修改顺序中看到相同或较晚的值。这可以防止编译时重新排序,并且一致的缓存 + 硬件保证可以免费实现这一点(无需任何额外的屏障指令),即使在非 x86 ISA 上也是如此。
编译器可以并且确实围绕relaxed原子加载/存储对其他变量上的其他内存操作进行重新排序。
volatileGCC/clang也是如此(以及 MSVC,/volatile:iso以确保它不会将其视为acquire/release即使在编译 x86 时)。但std::atomicwithrelaxed可移植地准确表达了您想要的语义,因此未来的编译器可能会更好地优化。而且无论位于哪个位置,volatile操作都无法在编译时与其他操作重新排序,这与原子操作不同。volatilerelaxed
std::atomic即使在非 x86 上,加载也只是普通的 asm 加载,至少对于像uint64_tx86-64 这样的单个寄存器的类型来说是这样。
不过,当前的 GCC 和 Clang 确实有一些小的遗漏优化,比如它们不会使用 astd::atomic<>.load作为另一条指令的内存源操作数 ( https://godbolt.org/z/sf8WcG7qs )
#include <atomic>
using T = std::atomic<long>;
long read(T &a, long dummy){
return dummy + a.load(std::memory_order_relaxed) + a.load(std::memory_order_relaxed);
}
Run Code Online (Sandbox Code Playgroud)
# GCC13.2 -O3 ; clang is equivalent
read(std::atomic<long long>&, long):
mov rax, QWORD PTR [rdi]
mov rdx, QWORD PTR [rdi]
add rax, rsi
add rax, rdx
ret
Run Code Online (Sandbox Code Playgroud)
与
long read_plain(long *p, long *q, long dummy){
return dummy + *p + *q;
}
Run Code Online (Sandbox Code Playgroud)
## GCC13 -O3 ; clang is similar
read_plain(long*, long*, long):
mov rax, QWORD PTR [rsi]
add rdx, QWORD PTR [rdi] ### memory source operand
add rax, rdx
ret
Run Code Online (Sandbox Code Playgroud)
如果他们能解决这样的问题就好了;没有理由a.store(1 + a.load(relaxed), relaxed);不应该编译为add qword ptr [rdi], 1(没有,lock因为我没有使用a.fetch_add),但是GCC将执行单独的加载/包含/存储,就像与普通一样volatile,但与普通不同long。
Clang 实际上将用于inc qword ptr [rdi]原子加载/添加/存储,因此在这种情况下只有 GCC 和 MSVC 错过了优化。
Clang 还将使用volatile加载作为 的内存源操作数add,例如
long read_volatile(volatile long *p, volatile long *q, long dummy){
return dummy + *p + *q;
}
Run Code Online (Sandbox Code Playgroud)
# clang 17 -O3
read_volatile(long volatile*, long volatile*, long):
mov rax, rdx
add rax, qword ptr [rdi] # same asm it makes for non-volatile
add rax, qword ptr [rsi] # unlike GCC which makes asm like for atomic
ret
Run Code Online (Sandbox Code Playgroud)
是的,这个 asm 可以说比 GCC 的普通情况更糟糕:在没有 mov-elimination 的 CPU 上,它有更多的后端 uops(每个 2 个add,如果没有消除则加上 1 个mov),并且mov将处于延迟的关键路径上从dummy到 结果。volatile但这个选择与vs.无关atomic。
因此,您现在可能会考虑使用volatile,特别是如果您使用 Clang,因为您正在针对 CISC ISA (x86) 进行编译,其中将加载折叠到其他指令的内存源操作数中可以节省前端带宽。
| 归档时间: |
|
| 查看次数: |
138 次 |
| 最近记录: |