代码之家  ›  专栏  ›  技术社区  ›  MarkB

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

  •  5
  • MarkB  · 技术社区  · 2 年前

    以下代码生成的程序集在使用编译时有条件地执行GCC 12.3中的SIMD -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];
        }
    }
    

    这是 link 在戈德堡。

    以下是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)。

    1 回复  |  直到 2 年前
        1
  •  4
  •   Peter Cordes    2 年前

    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 将其转换为其他字符串上的空字符串。)您可以使用 限制 具有引用和指针。(至少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];
        }
    }
    

    或者手动读取所有 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
    }
    

    这两者都编译到与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
    

    承诺一致性(如 alignas(16) 其中一种?)可以让它使用 paddd xmm1, [rdi] ,内存源操作数,不带AVX。

    推荐文章