以下代码生成的程序集在使用-O3
. 为了完整起见,代码始终在 GCC 13.2 中执行 SIMD,而从不在 clang 17.0.1 中执行 SIMD。
#include <array>
__attribute__((noinline)) void fn(std::array<int, 4>& lhs, const std::array<int, 4>& rhs)
{
for (std::size_t idx = 0; idx != 4; ++idx) {
lhs[idx] = lhs[idx] + rhs[idx];
}
}
这是Godbolt 中的链接。
这是 GCC 12.3 的实际汇编(使用 -O3):
fn(std::array<int, 4ul>&, std::array<int, 4ul> const&):
lea rdx, [rsi+4]
mov rax, rdi
sub rax, rdx
cmp rax, 8
jbe .L2
movdqu xmm0, XMMWORD PTR [rsi]
movdqu xmm1, XMMWORD PTR [rdi]
paddd xmm0, xmm1
movups XMMWORD PTR [rdi], xmm0
ret
.L2:
mov eax, DWORD PTR [rsi]
add DWORD PTR [rdi], eax
mov eax, DWORD PTR [rsi+4]
add DWORD PTR [rdi+4], eax
mov eax, DWORD PTR [rsi+8]
add DWORD PTR [rdi+8], eax
mov eax, DWORD PTR [rsi+12]
add DWORD PTR [rdi+12], eax
ret
我非常想知道 a) 前 5 条汇编指令的目的,以及 b) 是否可以采取任何措施使 GCC 12.3 发出 GCC 13.2 的代码(理想情况下,无需手动编写 SSE)。
看起来 GCC12 正在将引用
class
视为简单的,就和是否可以部分int *
重叠而言。lhs
rhs
精确重叠就可以了,如果
lhs[idx]
与 int 相同rhs[idx]
,我们在写入之前读取它两次。但对于部分重叠,rhs[3]
例如可以通过其中一个lhs[0..2]
添加项进行更新,如果我们在任何存储之前先完成所有加载,则 SIMD 就不会发生这种情况。GCC13 知道类对象不允许部分重叠(除了不同结构/类类型的常见初始序列内容,我认为这不适用于此处)。那就是 UB,所以它可以假设它不会发生。GCC12 的代码生成是一个错过的优化。
那么我们如何帮助GCC12呢?
__restrict
当编译器不想发明检查+后备时,通常的做法是删除重叠检查或启用自动向量化。在 C 中,restrict
是语言的一部分,但在 C++ 中它只是一种扩展。(主要主流编译器都支持,并且您可以使用预处理器将#define
其转换为其他编译器上的空字符串。)您可以__restrict
与引用一起使用,也可以与指针一起使用。(至少 GCC 和 Clang 接受它,没有警告-Wall
;我没有仔细检查文档以确保这是标准的。)lhs
或者在写入任何内容之前手动阅读全部内容由于您的
array
数据足够小,可以放入一个 SIMD 寄存器中,因此复制效率不会低下。array<int, 1000>
这会对什么不利!这两个都编译为与 GCC13 相同的自动矢量化 asm,没有浪费的指令 ( Godbolt )
有前途的对齐(比如
alignas(16)
其中一种类型?)可以让它使用paddd xmm1, [rdi]
,一个内存源操作数,而不需要 AVX。