浮点数向量的SSE降维

9

我可以用SSE指令来获取浮点向量元素的和(降维)。以下是简单的串行代码:

void(float *input, float &result, unsigned int NumElems)
{
     result = 0;
     for(auto i=0; i<NumElems; ++i)
         result += input[i];
}

3
你有尝试过什么吗? - harold
2
你是否真正查看了生成的代码?至少我的经验是,gcc在可能的情况下执行SSE指令做得非常好 - 但可能需要-O3。 - Mats Petersson
1个回答

20

通常在循环中会生成4个部分和,然后在循环后水平地加总这4个元素,例如:

一般在循环中生成四个部分和,之后只需要对循环后的这四个元素进行横向求和即可,例如:

#include <cassert>
#include <cstdint>
#include <emmintrin.h>

float vsum(const float *a, int n)
{
    float sum;
    __m128 vsum = _mm_set1_ps(0.0f);
    assert((n & 3) == 0);
    assert(((uintptr_t)a & 15) == 0);
    for (int i = 0; i < n; i += 4)
    {
        __m128 v = _mm_load_ps(&a[i]);
        vsum = _mm_add_ps(vsum, v);
    }
    vsum = _mm_hadd_ps(vsum, vsum);
    vsum = _mm_hadd_ps(vsum, vsum);
    _mm_store_ss(&sum, vsum);
    return sum;
}
注意:对于上面的示例,a 必须是16字节对齐的,并且n必须是4的倍数。如果无法保证 a 的对齐,则应使用 _mm_loadu_ps 代替 _mm_load_ps。如果不能保证 n 是4的倍数,则在函数末尾添加一个标量循环以累加剩余元素。

2
如果输入数组可能很大,最好在开始时有一个标量循环,运行0-3次,直到输入在SSE循环上对齐为止。然后您就不会有跨越缓存/页面线的负载减慢循环速度。它可以使用带有内存操作数的ADDPS,这可能会微型熔合,从而降低开销。此外,您可以通过使用多个累加器来获得2或4个依赖链,因此您的循环可以每个周期维持1个向量FP加法,而不是每个(ADDPS的延迟= 3)。 - Peter Cordes
@PeterCordes 你能解释一下 _mm_add_ps 和内存操作数的关系吗?你是指一个指针参数而不是 __m128 吗? - user2023370
2
@user2023370:我指的是汇编指令addps,而不是内部函数_mm_add_ps。我在谈论编译器的代码生成选项。例如,将_mm_load_ps折叠成可以微融合到解码器中的内存操作数,如addps xmm0,[rdi],而不是movups xmm1,[rdi]/addps xmm0,xmm1。(除非您拥有AVX以允许不对齐的内存操作数,否则无法折叠_mm_loadu_ps。)当然,您仍然绝对需要多个累加器来隐藏FP加法延迟:请参见为什么Haswell上的mulss只需要3个周期,与Agner的指令表不同?获取更多信息。 - Peter Cordes
谢谢。这非常有帮助。我也读了你对另一个问题的回答。我可以确认一下我的理解:如果我在上面的答案中添加一个累加器,我们可能会有一个v1v2,以及一个vsum1vsum2,循环增量为8而不是4? - user2023370

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接