为什么简单的 FP 循环不自动矢量化,并且比 SIMD 内在函数计算慢?

问题描述 投票:0回答:1

(为什么?)即使使用

-03 and -march=native
进行编译,编译器也不使用 SIMD 指令来计算总和的简单循环吗?

考虑以下两个函数:

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 上也是如此。请参阅此处获取代码。

我使用 GCC 13 和 Clang 17 编译了程序并使用了选项

-O3 -march=native

为什么函数

normal_sum
较慢且未完全矢量化?我需要指定额外的编译器选项吗?

c++ optimization x86-64 compiler-optimization simd
1个回答
4
投票

为什么函数 normal_sum 较慢且未完全矢量化?我需要指定额外的编译器选项吗?

是的。

-ffast-math
解决了这个问题(参见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 求和算法这样的代码要求编译器不要假设浮点运算是关联的(
-ffast-math
确实如此)。 有关这方面的更多信息,请阅读文章 gcc 的 ffast-math 实际上是做什么的?.

在没有

-ffast-math
的情况下,代码不会自动矢量化的主要原因很简单,因为像求和这样的浮点运算是 不是关联的 (即
(a+b)+c != a+(b+c)
)。因此,编译器无法对长浮点加法链重新排序。请注意,有一个标志专门用于更改此行为 (
-fassociative-math
),但通常不足以自动矢量化代码(此处就是这种情况)。需要使用标志组合(
-ffast-math
的子集)来启用有关目标编译器(可能还包括其版本)的自动向量化。

非快速数学也是为什么手动向量化版本中的水平求和循环效率如此低下的原因,一次洗牌以提取每个元素 1,而不是洗牌以将 SIMD 总和缩小一半。请参阅进行水平 SSE 向量和(或其他缩减)的最快方法


一种与架构无关的简单的代码矢量化方法是使用 OpenMP。为此,您需要在循环之前添加行

#pragma omp simd reduction (+:sum)
和编译标志
-fopenmp-simd
(或完整的
-fopenmp
,这也将让它尝试自动并行化)。 参见 Godbolt

即使没有

reduction

 子句,
Clang 也恰好接受它,但这并不能保证有效,甚至不能保证产生正确的结果。您实际上需要将归约子句中的
+
与您在源中执行的操作相匹配:至少对于整数,如果您使用
reduction (*:sum)
(&:sum)
,它知道起始
sum = 0
次或 AND任何等于 0 的值,因此 优化为只返回 0,即使循环体是
sum += vec[i]

无论如何,对于浮点,

omp simd reduction
告诉编译器可以假装对该变量的操作是关联的,仅对于此循环而言。程序的其余部分仍然可以具有严格的 FP 语义。

© www.soinside.com 2019 - 2024. All rights reserved.