是否可以在 AVX/SSE 中得到多个正弦?

Is it possible to get multiple sines in AVX/SSE?

我正在尝试编写一个 C++ 程序,它启动我在 x64 汇编器中编写的函数。 我想加快速度(并使用 CPU 功能),所以我选择使用矢量运算。

问题是,我必须将正弦乘以一个整数,所以我必须先计算正弦。 是否可以在 SSE/AVX 中执行此操作?我知道指令 fsin,但它不仅在 FPU 中,而且一次只计算 1 个正弦。所以我必须将它推入 FPU,调用 fsin,将其从 FPU 弹出到内存,然后将其放入 AVX 寄存器。在我看来,这不值得麻烦。

SSE/AVX 中没有正弦指令,但是根据您需要的精度,您可以使用 Taylor/Madhava series or as the quotient of two polynomials using Pade Approximant 将正弦函数的近似值写为多项式。当然还有更多的多项式逼近技术。

这是否会产生您想要的精度以及此方法的速度取决于您的具体问题。一般来说,多项式逼近非常快,因为可以使用 n 个 FMA 指令(Pade 逼近也需要一次除法)通过将其写成

的形式来评估 n 次多项式

a+x*(b+x*(c+x*(...))).

然而,正弦在使用多项式进行近似时表现出了众所周知的不良行为,因此用例受到限制。

是的,有一个矢量版本使用SSE/AVX! 但问题是必须使用 Intel C++ 编译器。

这叫做 Intel 小型矢量数学库(内在函数):

对于 128 位 SSE,请使用(双精度):_mm_sin_pd

对于 256 位 AVX,请使用(双精度):_mm256_sin_pd

这两个内部函数实际上是非常小的函数,由手写的 SSE/AVX 程序集组成,现在您可以使用 AVX 一次处理 4 个正弦计算 :=) 延迟约为 10 个时钟周期(如果我没记错)在 Haswell CPU。

顺便说一下,CPU 需要执行大约 100 个这样的内部函数来预热并达到其最佳性能,如果只需要评估几个 sin 函数,最好使用普通 sin () 反而。

祝你好运!!

由于 OpenMP 4.0 需要矢量化 sin/cos 扩展,gcc-glibc 也在 libmvec 中提供了它们。参见:

有关其他 SVML 备选方案的列表,请参阅

样本余弦近似:

  • 0.15 ULPS 平均值(1 ULPS 最大值)误差
  • AVX512 加速 12 倍,AVX2 加速 8-9 倍 [-1,1] 范围内余弦的近似值。

多项式系数是通过遗传算法找到的。乘法级数看起来像 Chebyshev 多项式,但它们不是,但是当您需要更广泛的输入范围而不仅仅是 [-1,1].

时需要它们
    // only optimized for [-1,1] input range!!
    template<typename Type, int Simd>
    inline
    void cosFast(
       Type * const __restrict__ data,
       Type * const __restrict__ result) noexcept
    {
        alignas(64)
        Type xSqr[Simd];

        alignas(64)
        Type xSqrSqr[Simd];

        alignas(64)
        Type xSqrSqrSqr[Simd];

        alignas(64)
        Type xSqrSqrSqrSqr[Simd];

        #pragma GCC ivdep
        for(int i=0;i<Simd;i++)
        {
            xSqr[i] =   data[i]*data[i];
        }

        #pragma GCC ivdep
        for(int i=0;i<Simd;i++)
        {
            xSqrSqr[i] =    xSqr[i]*xSqr[i];
        }

        #pragma GCC ivdep
        for(int i=0;i<Simd;i++)
        {
            xSqrSqrSqr[i] =     xSqrSqr[i]*xSqr[i];
        }


        #pragma GCC ivdep
        for(int i=0;i<Simd;i++)
        {
            xSqrSqrSqrSqr[i] =  xSqrSqr[i]*xSqrSqr[i];
        }

        #pragma GCC ivdep
        for(int i=0;i<Simd;i++)
        {
            result[i] =     Type(2.37711074060342753000441e-05)*xSqrSqrSqrSqr[i] +
                                Type(-0.001387712893937020908197155)*xSqrSqrSqr[i] +
                                Type(0.04166611039514833692010143)*xSqrSqr[i] +
                                Type(-0.4999998698566363586337502)*xSqr[i] +
                                Type(0.9999999941252593060880827);
        }
    }

如果你需要使用更宽的范围,那么你应该在高精度下做一个range-reduction并使用这样的东西:

  • 范围缩小到 -pi,pi 高精度
  • 除以“4”使其在-1,1范围内
  • 计算与上面相同的系列 => tmp
  • 计算(切比雪夫)L_"4" (tmp) = 结果

 .L29:
        vmovups ymm7, YMMWORD PTR [r14+rax]
        vmulps  ymm1, ymm7, ymm7
        vmovups ymm7, YMMWORD PTR [r14+32+rax]
        vmulps  ymm3, ymm1, ymm1
        vmulps  ymm6, ymm3, ymm3
        vmulps  ymm6, ymm6, YMMWORD PTR .LC11[rip]
        vmulps  ymm0, ymm7, ymm7
        vmulps  ymm5, ymm3, ymm1
        vfmadd132ps     ymm5, ymm6, YMMWORD PTR .LC12[rip]
        vmulps  ymm2, ymm0, ymm0
        vmulps  ymm6, ymm2, ymm2
        vmulps  ymm6, ymm6, YMMWORD PTR .LC11[rip]
        vfmadd132ps     ymm3, ymm5, YMMWORD PTR .LC13[rip]
        vmulps  ymm4, ymm2, ymm0
        vfmadd132ps     ymm4, ymm6, YMMWORD PTR .LC12[rip]
        vfmadd132ps     ymm1, ymm3, YMMWORD PTR .LC14[rip]
        vfmadd132ps     ymm2, ymm4, YMMWORD PTR .LC13[rip]
        vaddps  ymm1, ymm1, YMMWORD PTR .LC15[rip]
        vfmadd132ps     ymm0, ymm2, YMMWORD PTR .LC14[rip]
        vaddps  ymm0, ymm0, YMMWORD PTR .LC15[rip]
        vmovups YMMWORD PTR [r15+rax], ymm1
        vmovups YMMWORD PTR [r15+32+rax], ymm0
        add     rax, 64
        cmp     rax, 16777216
        jne     .L29