【问题标题】:Complex Mul and Div using sse Instructions使用 sse 指令的复杂 Mul 和 Div
【发布时间】:2011-03-13 18:51:12
【问题描述】:

通过 SSE 指令执行复数乘法和除法是否有益? 我知道加法和减法在使用 SSE 时表现更好。谁能告诉我如何使用 SSE 执行复数乘法以获得更好的性能?

【问题讨论】:

    标签: x86 sse simd complex-numbers


    【解决方案1】:

    为了完整起见,可下载here 的英特尔® 64 和 IA-32 架构优化参考手册包含复数乘法(示例 6-9)和复数除法(示例 6-10)的汇编。

    下面是乘法代码:

    // Multiplication of (ak + i bk ) * (ck + i dk )
    // a + i b can be stored as a data structure
    movsldup xmm0, src1; load real parts into the destination, a1, a1, a0, a0
    movaps xmm1, src2; load the 2nd pair of complex values, i.e. d1, c1, d0, c0
    mulps xmm0, xmm1; temporary results, a1d1, a1c1, a0d0, a0c0
    shufps xmm1, xmm1, b1; reorder the real and imaginary parts, c1, d1, c0, d0
    movshdup xmm2, src1; load imaginary parts into the destination, b1, b1, b0, b0
    mulps xmm2, xmm1; temporary results, b1c1, b1d1, b0c0, b0d0
    addsubps xmm0, xmm2; b1c1+a1d1, a1c1 -b1d1, b0c0+a0d0, ; a0c0-b0d0
    

    程序集直接映射到gccs X86 intrinsics(只需用__builtin_ia32_ 谓词每条指令)。

    【讨论】:

    • 理想情况下,将您的数据与实数和图像分开存储,这样您就可以加载一个由 4 个实部组成的向量和一个由 4 个图像部分组成的向量,并且可以并行处理 4 个产品而无需改组。如果使用像 C complex float 这样的 SIMD 不友好的打包/交错的原始存储格式,您只需要这样的东西。
    【解决方案2】:

    复数乘法定义为:

    ((c1a * c2a) - (c1b * c2b)) + ((c1b * c2a) + (c1a * c2b))i
    

    所以你的复数中的 2 个分量是

    ((c1a * c2a) - (c1b * c2b)) and ((c1b * c2a) + (c1a * c2b))i
    

    所以假设你使用 8 个浮点数来表示 4 个复数,定义如下:

    c1a, c1b, c2a, c2b
    c3a, c3b, c4a, c4b
    

    如果您想同时执行 (c1 * c3) 和 (c2 * c4),您的 SSE 代码会看起来像下面这样:

    (注意我在windows下使用MSVC,但原理是一样的)。

    __declspec( align( 16 ) ) float c1c2[]        = { 1.0f, 2.0f, 3.0f, 4.0f };
    __declspec( align( 16 ) ) float c3c4[]          = { 4.0f, 3.0f, 2.0f, 1.0f };
    __declspec( align( 16 ) ) float mulfactors[]    = { -1.0f, 1.0f, -1.0f, 1.0f };
    __declspec( align( 16 ) ) float res[]           = { 0.0f, 0.0f, 0.0f, 0.0f };
    
    __asm 
    {
        movaps xmm0, xmmword ptr [c1c2]         // Load c1 and c2 into xmm0.
        movaps xmm1, xmmword ptr [c3c4]         // Load c3 and c4 into xmm1.
        movaps xmm4, xmmword ptr [mulfactors]   // load multiplication factors into xmm4
    
        movaps xmm2, xmm1                       
        movaps xmm3, xmm0                       
        shufps xmm2, xmm1, 0xA0                 // Change order to c3a c3a c4a c4a and store in xmm2
        shufps xmm1, xmm1, 0xF5                 // Change order to c3b c3b c4b c4b and store in xmm1
        shufps xmm3, xmm0, 0xB1                 // change order to c1b c1a c2b c2a abd store in xmm3
    
        mulps xmm0, xmm2                        
        mulps xmm3, xmm1                    
        mulps xmm3, xmm4                        // Flip the signs of the 'a's so the add works correctly.
    
        addps xmm0, xmm3                        // Add together
    
        movaps xmmword ptr [res], xmm0          // Store back out
    };
    
    float res1a = (c1c2[0] * c3c4[0]) - (c1c2[1] * c3c4[1]);
    float res1b = (c1c2[1] * c3c4[0]) + (c1c2[0] * c3c4[1]);
    
    float res2a = (c1c2[2] * c3c4[2]) - (c1c2[3] * c3c4[3]);
    float res2b = (c1c2[3] * c3c4[2]) + (c1c2[2] * c3c4[3]);
    
    if ( res1a != res[0] || 
         res1b != res[1] || 
         res2a != res[2] || 
         res2b != res[3] )
    {
        _exit( 1 );
    }
    

    我在上面所做的只是稍微简化了数学运算。假设如下:

    c1a c1b c2a c2b
    c3a c3b c4a c4b
    

    通过重新排列,我最终得到以下向量

    0 => c1a c1b c2a c2b
    1 => c3b c3b c4b c4b
    2 => c3a c3a c4a c4a
    3 => c1b c1a c2b c2a
    

    然后我将 0 和 2 相乘得到:

    0 => c1a * c3a, c1b * c3a, c2a * c4a, c2b * c4a
    

    接下来我将 3 和 1 相乘得到:

    3 => c1b * c3b, c1a * c3b, c2b * c4b, c2a * c4b
    

    最后我翻转了 3 中几个花车的标志

    3 => -(c1b * c3b), c1a * c3b, -(c2b * c4b), c2a * c4b
    

    所以我可以把它们加在一起得到

    (c1a * c3a) - (c1b * c3b), (c1b * c3a ) + (c1a * c3b), (c2a * c4a) - (c2b * c4b), (c2b * c4a) + (c2a * c4b)
    

    这就是我们所追求的 :)

    【讨论】:

    • 另见software.intel.com/file/1000,它似乎有更快的算法。
    • 是的,与我的设置类似,但他们的设置需要 SSE3 ......我承认,在这个时代,99% 的时间都可以。
    • 那个 addsubps 指令看起来很方便。唉,出于兼容性原因,我通常不会将目标定位在 SSE2 之上:(
    【解决方案3】:

    英特尔优化参考中的算法无法正确处理输入中的溢出和NaNs。

    数字的实部或虚部中的单个NaN 将错误地传播到另一部分。

    由于多个无穷大(例如无穷大 * 0)的运算以 NaN 结尾,因此溢出可能会导致 NaNs 出现在您原本表现良好的数据中。

    如果溢出和NaNs 很少见,避免这种情况的简单方法是检查结果中的NaN 并使用符合IEEE 标准的编译器实现重新计算:

    float complex a[2], b[2];
    __m128 res = simd_fast_multiply(a, b);
    
    /* store unconditionally, can be executed in parallel with the check
     * making it almost free if there is no NaN in data */
    _mm_store_ps(dest, res);
    
    /* check for NaN */
    __m128 n = _mm_cmpneq_ps(res, res);
    int have_nan = _mm_movemask_ps(n);
    if (have_nan != 0) {
        /* do it again unvectorized */
        dest[0] = a[0] * b[0];
        dest[1] = a[1] * b[1];
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2010-10-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多