【问题标题】:How to Calculate single-vector Dot Product using SSE intrinsic functions in C如何在 C 中使用 SSE 内在函数计算单向量点积
【发布时间】:2011-05-06 11:04:41
【问题描述】:

我试图将两个向量相乘,其中一个向量的每个元素都乘以另一个向量的相同索引中的元素。然后我想对结果向量的所有元素求和以获得一个数字。例如,向量 {1,2,3,4} 和 {5,6,7,8} 的计算如下所示:

1*5 + 2*6 + 3*7 + 4*8

基本上,我正在取两个向量的点积。我知道有一个 SSE 命令可以执行此操作,但该命令没有与之关联的内在函数。此时,我不想在我的 C 代码中编写内联汇编,所以我只想使用内部函数。这似乎是一个常见的计算,所以我对自己在 Google 上找不到答案感到惊讶。

注意:我正在针对支持 SSE 4.2 的特定微架构进行优化。

【问题讨论】:

    标签: c optimization vectorization sse simd


    【解决方案1】:

    如果您要计算较长向量的点积,请在内循环中使用乘法和正则 _mm_add_ps(或 FMA)。 将水平和保存到最后。


    但如果您只对一对 SIMD 向量进行点积:

    GCC(至少 4.3 版)包含具有 SSE4.1 级别内在函数的 <smmintrin.h>,包括单精度和双精度点积:

    _mm_dp_ps (__m128 __X, __m128 __Y, const int __M);
    _mm_dp_pd (__m128d __X, __m128d __Y, const int __M);
    

    在 Intel 主流 CPU(不是 Atom/Silvermont)上,这些比手动执行多条指令要快一些。

    但在 AMD(包括 Ryzen)上,dpps 的速度明显较慢。 (见Agner Fog's instruction tables


    作为旧处理器的后备方案,您可以使用此算法创建向量 ab 的点积:

    __m128 r1 = _mm_mul_ps(a, b);
    

    然后水平求和 r1 使用 Fastest way to do horizontal float vector sum on x86(请参阅那里的评论版本,以及为什么它更快。)

    __m128 shuf   = _mm_shuffle_ps(r1, r1, _MM_SHUFFLE(2, 3, 0, 1));
    __m128 sums   = _mm_add_ps(r1, shuf);
    shuf          = _mm_movehl_ps(shuf, sums);
    sums          = _mm_add_ss(sums, shuf);
    float result =  _mm_cvtss_f32(sums);
    

    一个缓慢的替代方案每个hadd 需要 2 次 shuffle,这很容易成为 shuffle 吞吐量的瓶颈,尤其是在 Intel CPU 上。

    r2 = _mm_hadd_ps(r1, r1);
    r3 = _mm_hadd_ps(r2, r2);
    _mm_store_ss(&result, r3);
    

    【讨论】:

    • 作为说明,我想指出使用 dp 内在函数计算点积比使用第二种方法要慢。
    • @SergueiFedorov 完全取决于您的硬件,没有全局情况表明它较慢。
    • 我认为横向求和有比使用_mm_hadd_ps更好的方法。见stackoverflow.com/a/35270026/195787
    【解决方案2】:

    我会说最快的 SSE 方法是:

    static inline float CalcDotProductSse(__m128 x, __m128 y) {
        __m128 mulRes, shufReg, sumsReg;
        mulRes = _mm_mul_ps(x, y);
    
        // Calculates the sum of SSE Register - https://stackoverflow.com/a/35270026/195787
        shufReg = _mm_movehdup_ps(mulRes);        // Broadcast elements 3,1 to 2,0
        sumsReg = _mm_add_ps(mulRes, shufReg);
        shufReg = _mm_movehl_ps(shufReg, sumsReg); // High Half -> Low Half
        sumsReg = _mm_add_ss(sumsReg, shufReg);
        return  _mm_cvtss_f32(sumsReg); // Result in the lower part of the SSE Register
    }
    

    我关注了 - Fastest Way to Do Horizontal Float Vector Sum On x86

    【讨论】:

    • 很棒的发现,hadd 指令扩展到了几个微指令。
    【解决方案3】:

    我写了这个并用gcc -O3 -S -ftree-vectorize -ftree-vectorizer-verbose=2 sse.c编译了它

    void f(int * __restrict__ a, int * __restrict__ b, int * __restrict__ c, int * __restrict__ d,
           int * __restrict__ e, int * __restrict__ f, int * __restrict__ g, int * __restrict__ h,
           int * __restrict__ o)
    {
        int i;
    
        for (i = 0; i < 8; ++i)
            o[i] = a[i]*e[i] + b[i]*f[i] + c[i]*g[i] + d[i]*h[i];
    }
    

    GCC 4.3.0 自动矢量化了它:

    sse.c:5: note: LOOP VECTORIZED.
    sse.c:2: note: vectorized 1 loops in function.
    

    但是,只有在我使用具有足够迭代次数的循环时才会这样做——否则详细的输出会表明矢量化无利可图或循环太小。如果没有 __restrict__ 关键字,它必须生成单独的非矢量化版本来处理输出 o 可能指向输入之一的情况。

    我会将说明粘贴为示例,但由于部分矢量化展开了循环,因此可读性不强。

    【讨论】:

    • 我认为他的意思是别的。就像 2 个数组,每个数组有 4 个元素。你在这里做的是另一回事。类似于向量数组的点积。
    【解决方案4】:

    英特尔here 有一篇文章涉及点积实现。

    【讨论】:

      猜你喜欢
      • 2015-08-15
      • 1970-01-01
      • 2012-07-04
      • 2021-07-11
      • 2017-10-06
      • 2012-11-05
      • 2017-10-21
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多