c++ 为什么一个简单的FP循环不能自动向量化,并且比SIMD内部函数计算慢?

tquggr8v  于 5个月前  发布在  其他
关注(0)|答案(1)|浏览(71)

(Why?)编译器是否不使用SIMD指令进行简单的循环计算和,即使在使用-03 and -march=native编译时也是如此?
考虑以下两个函数:

float sum_simd(const std::vector<float>& vec) {
    __m256 a{0.0};
    for (std::size_t i = 0; i < vec.size(); i += 8) {
        __m256 tmp = _mm256_loadu_ps(&vec[i]);
        a = _mm256_add_ps(tmp, a);
    }
    float res{0.0};
    for (size_t i = 0; i < 8; ++i) {
        res += a[i];
    }
    return res;
}

float normal_sum(const std::vector<float>& vec) {
    float sum{0};
    for (size_t i = 0; i < vec.size(); ++i) {
        sum += vec[i];
    }
    return sum;
}

字符串
编译器似乎将求和转换为:

vaddps  ymm0, ymm0, ymmword ptr [rax + 4*rdx]


vaddss  xmm0, xmm0, dword ptr [rcx + 4*rsi]
vaddss  xmm0, xmm0, dword ptr [rcx + 4*rsi + 4]
vaddss  xmm0, xmm0, dword ptr [rcx + 4*rsi + 8]
vaddss  xmm0, xmm0, dword ptr [rcx + 4*rsi + 12]
vaddss  xmm0, xmm0, dword ptr [rcx + 4*rsi + 16]
vaddss  xmm0, xmm0, dword ptr [rcx + 4*rsi + 20]
vaddss  xmm0, xmm0, dword ptr [rcx + 4*rsi + 24]
vaddss  xmm0, xmm0, dword ptr [rcx + 4*rsi + 28]


当我在我的机器上运行这个程序时,我从SIMD和中获得了相当大的加速(~ 10倍)。在Godbolt上也是如此。代码见here
我用GCC 13和Clang 17编译了这个程序,并使用了-O3 -march=native选项。
为什么函数normal_sum速度较慢且没有完全向量化?我需要指定其他编译器选项吗?

5f0d552i

5f0d552i1#

为什么函数normal_sum速度较慢且没有完全向量化?我需要指定其他编译器选项吗?
是的。**-ffast-math**解决了这个问题(see on Godbolt)。下面是带有这个附加标志的主循环:

.L10:
        vaddps  ymm1, ymm1, YMMWORD PTR [rax]     ;     <---------- vectorized
        add     rax, 32
        cmp     rcx, rax
        jne     .L10

字符串
但是,请注意,-ffast-math是几个更具体的标志的组合。其中一些标志可能相当危险。例如,-funsafe-math-optimizations-ffinite-math-only可能会破坏使用无穷大的现有代码或降低其精度。实际上,有些代码(如Kahan summation algorithm)要求编译器不要假设浮点运算是关联的。(这-ffast-math做).有关此的更多信息,请阅读后What does gcc's ffast-math actually do? .
代码在没有-ffast-math的情况下不会自动矢量化的主要原因是因为像求和这样的浮点运算是非关联的。(即(a+b)+c != a+(b+c))。因此,编译器无法重新排序浮点加法的长链。请注意,有一个标志专门用于更改此行为(-fassociative-math),但通常不足以自动向量化代码(this is the case here)。需要使用标志的组合(-ffast-math的子集)来启用关于目标编译器(可能还有其版本)的自动向量化。
非快速数学运算也是手动矢量化版本中的水平求和循环效率如此低下的原因,它每次都是通过重排来提取每个元素1,而不是通过重排来缩小SIMD和的一半。
一个简单的架构无关的矢量化代码的方法是使用OpenMP。要做到这一点,你需要在循环之前添加#pragma omp simd reduction (+:sum)行和编译标志-fopenmp-simd(或完整的-fopenmp,这也会让它尝试自动并行化)。请参阅Godbolt。
即使没有reduction子句,Clang也会接受它,但这并不能保证工作,甚至不能产生正确的结果。实际上,您需要将reduction子句中的+与源代码中的操作相匹配:至少对于整数,如果你使用reduction (*:sum)(&:sum),它知道开始的sum = 0乘以或AND任何等于0,所以optimizes to just return 0即使循环体是sum += vec[i]
无论如何,对于浮点数,omp simd reduction告诉编译器,对于这个循环,可以假装对该变量的操作是关联的。程序的其余部分仍然可以具有严格的FP语义。

相关问题