考虑使用以下编译的示例-O3 -march=native
:
struct str{
volatile uint64_t a1;
volatile uint64_t a2;
volatile uint64_t a3;
volatile uint64_t a4;
};
int main(void){
struct str str1;
struct str str2;
str1.a1 = str2.a2;
str1.a2 = str2.a2;
str1.a3 = str2.a3;
str1.a4 = str2.a4;
}
它产生以下汇编代码:
main:
push rbp
vpxor xmm0, xmm0, xmm0
vmovdqu8 YMMWORD PTR [rsp-32], ymm0
mov rbp, rsp
mov rax, QWORD PTR [rsp-24]
mov QWORD PTR [rsp-64], rax
mov rax, QWORD PTR [rsp-24]
mov QWORD PTR [rsp-56], rax
mov rax, QWORD PTR [rsp-16]
mov QWORD PTR [rsp-48], rax
mov rax, QWORD PTR [rsp-8]
mov QWORD PTR [rsp-40], rax
xor eax, eax
vzeroupper
pop rbp
ret
在我的机器上KbL i7-8550U
它产生几乎相同的机器代码:
(gdb) disas main
Dump of assembler code for function main:
pxor xmm0,xmm0
movaps XMMWORD PTR [rsp-0x28],xmm0
mov rax,QWORD PTR [rsp-0x20]
movaps XMMWORD PTR [rsp-0x18],xmm0
mov QWORD PTR [rsp-0x48],rax
mov rax,QWORD PTR [rsp-0x20]
mov QWORD PTR [rsp-0x40],rax
mov rax,QWORD PTR [rsp-0x18]
mov QWORD PTR [rsp-0x38],rax
mov rax,QWORD PTR [rsp-0x10]
mov QWORD PTR [rsp-0x30],rax
xor eax,eax
ret
在我的机器上支持avx2
,但不SIMD
用于复制。
如何提示gcc
使用256 bit
based SIMD
(因为该结构的大小为 256 字节)?
GCC 将
volatile
在 asm 中使用单独的访问来进行每次访问,而不是作为 SIMD 向量的元素。如果这些是 MMIO 地址,那么这对于正确性是必要的。 通过使用volatile
,您将禁止 GCC 进行您想要的优化!要测试某些内容的编译方式,请编写一个接受指针或作用于全局变量的函数,如如何从 GCC/clang 程序集输出中删除“噪音”?。请参阅https://godbolt.org/z/5h9Gc9o9c:没有
volatile
,GCC 和 clang-march=skylake
使用 AVX2 进行随机播放,或者如果您不重复,则使用 256 位加载/存储直接复制a2
。如果您要单独使用
volatile
每个uint64_t
,您可以只使用_Atomic
(与memory_order_relaxed
或release
。)您不能指望它的排序,因为除了相对于其他 s 的易失性访问之外,它
volatile
不会做任何事情来阻止编译volatile
时的重新排序,因此您将无法获得有保证的获取/释放同步。很像memory_order_relaxed
。_Atomic uint64_t
使用memory_order_acquire
/release
会给你相同的代码生成,volatile
但具有保证的行为。 何时在多线程中使用 易失性?- 几乎永远不会,除非你无法得到_Atomic
/std::atomic<>
做出好的汇编,就像在这种情况下,它既不会volatile uint64_t
完全按照你想要的那样做。神箭
但要注意这在 C++ 中不起作用;结构赋值变成每个元素的副本,并且
std::atomic<>
的复制构造函数被删除。或者使用 时volatile
,隐式复制构造函数未标记volatile
,因此不会复制带有volatile
成员的结构。不幸的是,GCC 和 Clang 没有对向量加载/存储和收集/分散的每个元素原子性做出任何假设?所以
_Atomic uint64_t
成员会导致每个元素的复制,如果没有一些黑客代码,我不知道解决这个问题的好方法。https://godbolt.org/z/8zGE4soMe。(而且它们无论如何都不会优化原子;编译器内部可能很像对待原子volatile
,因为这是确保它们不会被优化的一种方法。)如果该结构按 16 对齐,那么在带有 AVX 的 CPU(至少是 Intel)上,在纸面上可以保证将其复制为两个 128 位的一半,并使用 或 来复制它,因为 Intel 最终开始记录
movaps
AVX意味着对齐访问的 128 位加载/存储原子性。 SSE指令:哪些CPU可以进行原子16B内存操作?vmovdqa
一个现实世界的黑客来获得你想要的汇编,相当安全/面向未来
考虑将结构对齐 32 字节(
_Alignas(32)
在第一个成员上),然后使用volatile __m256i*
它来复制它。(直接取消引用指针,不要使用_mm256_load_si256
。)这与 using 完全相同volatile uint64_t*
,但强制编译器执行一次 32 字节访问,而不是四次 8 字节访问。在现代 CPU 上,整个结构体副本实际上都是原子的,但 Alder Lake E 核除外,其中每个 128 位一半都是原子的。https://rigtorp.se/isatomic/。
我不知道有什么会在 8 字节块内撕裂,这就是您从 GCC 的解释中得到的全部信息,即
volatile
提供高达整数寄存器宽度的无撕裂访问,适合 Linux 内核将其用于原子的方式。(请参阅对此答案的评论,了解 GCC 避免在 AArch64 上使用非原子存储来存储易失性。)更重要的是,即使 CPU 将宽 SIMD 存储分成 8 字节或 16 字节块来单独存储,也没有一种合理的机制可以让 CPU 分解宽 SIMD 存储的元素。由于它是对齐的,因此 8 字节部分块也对齐,尤其是当整个向量自然对齐时。从理论上讲,访问宽于 16 字节(或宽于 8 字节,没有 AVX)的保证为零,因此 Deathstation 9000 x86 可能会破坏此类代码,但对同一个 8 字节块多次访问缓存没有实际意义。
在没有对齐的情况下使用
_mm256_loadu_si256
是行不通的,因为它不是volatile
. (在 GNU C 中,volatile
或多或少有明确的定义并支持滚动您自己的relaxed
原子。并且不要试图使用它asm("" ::: "memory")
来强制在非易失性访问周围进行加载或存储:请参阅谁害怕严重的优化编译器?一些比较晦涩的优化可能会让您感到困扰,例如为源代码读取一次的非易失性内容发明额外的负载。)使用您自己的
__attribute__((aligned(1),vector_size(32),may_alias))
未对齐向量允许 avolatile*
将是具有单指令加载/存储的 32 字节副本(或 16 字节一半,具体取决于调整设置)。但如果它仅按 8 对齐,那么在纸面上,即使在 8 字节元素内,也无法保证原子性。在实践中,元素之间可能会出现撕裂。可能最好让它自然地对齐到一个缓存行内,而不是跨缓存组或某些 AMD CPU 上的其他内容分割。