为什么 GCC 生成有条件执行 SIMD 实现的代码?

Mar*_*rkB 24 c++ gcc simd auto-vectorization

以下代码生成的程序集在使用-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];
    }
}
Run Code Online (Sandbox Code Playgroud)

这是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
Run Code Online (Sandbox Code Playgroud)

我非常想知道 a) 前 5 条汇编指令的目的,以及 b) 是否可以采取任何措施使 GCC 12.3 发出 GCC 13.2 的代码(理想情况下,无需手动编写 SSE)。

Pet*_*des 30

看起来 GCC12 正在将引用class视为简单的,就和是否可以部分int *重叠而言。lhsrhs

精确重叠就可以了,如果lhs[idx]与 int 相同rhs[idx],我们在写入之前读取它两次。但对于部分重叠,rhs[3]例如可以通过其中一个lhs[0..2]添加项进行更新,如果我们在任何存储之前先完成所有加载,则 SIMD 就不会发生这种情况。

GCC13 知道类对象不允许部分重叠(除了不同结构/类类型的常见初始序列内容,我认为这不适用于此处)。那就是 UB,所以它可以假设它不会发生。GCC12 的代码生成是一个错过的优化。


那么我们如何帮助GCC12呢?__restrict当编译器不想发明检查+后备时,通常的做法是删除重叠检查或启用自动向量化。在 C 中,restrict是语言的一部分,但在 C++ 中它只是一种扩展。(主要主流编译器都支持,并且您可以使用预处理器将#define其转换为其他编译器上的空字符串。)您可以__restrict与引用一起使用,也可以与指针一起使用。(至少 GCC 和 Clang 接受它,没有警告-Wall;我没有仔细检查文档以确保这是标准的。)

// downside: fn_restrict(same, same) would be UB
void fn_restrict(std::array<int, 4>&__restrict lhs, const std::array<int, 4>& rhs)
{
    for (std::size_t idx = 0; idx != 4; ++idx) {
        lhs[idx] = lhs[idx] + rhs[idx];
    }
}
Run Code Online (Sandbox Code Playgroud)

lhs或者在写入任何内容之前手动阅读全部内容

由于您的array数据足够小,可以放入一个 SIMD 寄存器中,因此复制效率不会低下。array<int, 1000>这会对什么不利!

// downside: only efficient for small arrays that fit in a few vector regs at most
void fn_temporary(std::array<int, 4>& lhs, const std::array<int, 4>& rhs)
{
    auto sum = lhs;    // read the possibly-aliasing data into a temporary
    for (std::size_t idx = 0; idx != 4; ++idx) {
        sum[idx] += rhs[idx];  // update the temporary
    }
    lhs = sum;   // store back, after all loads
}
Run Code Online (Sandbox Code Playgroud)

这两个都编译为与 GCC13 相同的自动矢量化 asm,没有浪费的指令 ( Godbolt )

# GCC12 -O3
fn_temporary(std::array<int, 4ul>&, std::array<int, 4ul> const&):
        movdqu  xmm0, XMMWORD PTR [rsi]
        movdqu  xmm1, XMMWORD PTR [rdi]
        paddd   xmm0, xmm1
        movups  XMMWORD PTR [rdi], xmm0
        ret
Run Code Online (Sandbox Code Playgroud)

有前途的对齐(比如alignas(16)其中一种类型?)可以让它使用paddd xmm1, [rdi],一个内存源操作数,而不需要 AVX。