【问题标题】:Why my SSE code is slower than native C++ code?为什么我的 SSE 代码比原生 C++ 代码慢?
【发布时间】:2019-02-08 20:19:39
【问题描述】:

首先,我是 SSE 的新手。我决定加速我的代码,但它似乎比我的本机代码运行得更慢。

这是一个计算平方和的示例。在我的 Intel i7-6700HQ 上,本地代码需要 0.43 秒,SSE 需要 0.52。那么,瓶颈在哪里呢?

inline float squared_sum(const float x, const float y)
{
    return x * x + y * y;
}

#define USE_SIMD

void calculations()
{
    high_resolution_clock::time_point t1, t2;

    int result_v = 0;

    t1 = high_resolution_clock::now();

    alignas(16) float data_x[4];
    alignas(16) float data_y[4];
    alignas(16) float result[4];
    __m128 v_x, v_y, v_res;
    for (int y = 0; y < 5120; y++)
    {
        data_y[0] = y;
        data_y[1] = y + 1;
        data_y[2] = y + 2;
        data_y[3] = y + 3;
        for (int x = 0; x < 5120; x++)
        {
            data_x[0] = x;
            data_x[1] = x + 1;
            data_x[2] = x + 2;
            data_x[3] = x + 3;
#ifdef USE_SIMD
            v_x = _mm_load_ps(data_x);
            v_y = _mm_load_ps(data_y);
            v_x = _mm_mul_ps(v_x, v_x);
            v_y = _mm_mul_ps(v_y, v_y);
            v_res = _mm_add_ps(v_x, v_y);
            _mm_store_ps(result, v_res);
#else
            result[0] = squared_sum(data_x[0], data_y[0]);
            result[1] = squared_sum(data_x[1], data_y[1]);
            result[2] = squared_sum(data_x[2], data_y[2]);
            result[3] = squared_sum(data_x[3], data_y[3]);
#endif

            result_v += (int)(result[0] + result[1] + result[2] + result[3]);
        }
    }

    t2 = high_resolution_clock::now();
    duration<double> time_span1 = duration_cast<duration<double>>(t2 - t1);
    std::cout << "Exec time:\t" << time_span1.count() << " s\n";
}

更新:根据 cmets 修复代码。

我使用的是 Visual Studio 2017。针对 x64 编译。

  • 优化:最大优化(优先速度)(/O2)
  • 内联函数扩展:任何合适的 (/Ob2)
  • 偏爱大小或速度:偏爱快速代码 (/Ot)
  • 省略帧指针:是 (/Oy)

结论

编译器生成已经优化的代码,因此现在很难进一步加速。为了进一步加速代码,您可以做的一件事是并行化。

感谢您的回答。它们基本相同,所以我接受 Søren V. Poulsen 的回答,因为这是第一个。

【问题讨论】:

  • 编译器is already using SIMD。 (将来,请让您的代码准备好复制/粘贴。这意味着修复像 tbb 这样的类型而不是 std##endif 而不是 #endif。)
  • @RaymondChen:我推测tbb 是英特尔“线程构建模块”使用的命名空间,而不是错误输入的std
  • 这并非不可能,但会非常困难。一种方法是优化算法,而不是尝试优化算法的步骤。例如,result 向量始终为{ x*x + y+y, (x+1)*(x+1) + (y+1)*(y+1), (x+2)*(x+2) + (y+2)*(y+2), (x+3)*(x+3)+(y+3)*(y+3) }y 部分在循环中不会改变,因此您不必继续计算它,x 部分可以降低强度。实际上,结果总是一样的,所以你可以预先计算整个事情并将result_v设置为最终答案,并且根本不运行循环。
  • @user1554270 确保您的性能测试实际上类似于您的实际用例。如果您动态生成数据或从内存中读取数据,则会产生显着差异。此外,编译器有时足够聪明,可以在编译时评估整个循环(特别是如果您在任何地方都使用整数)。
  • @chtz:是的,但是即使没有-ffast-math,GCC 也会与 FMA 签订合同(这是正确的技术术语,不是崩溃,哎呀)。这是合法的within ISO C 中的一个表达式(我假设是 C++),但在表达式之间不是严格合法的(例如嵌套内在函数或在单​​独的语句中分配给 tmp 变量。)How to use Fused Multiply-Add (FMA) instructions with SSE/AVX。默认情况下,允许编译器打开#pragma STDC FP_CONTRACT ON。但是 GCC 仍然在这里违反规则。不过,这通常是件好事。

标签: c++ sse simd


【解决方案1】:

现代编译器是令人难以置信的机器,如果可能的话,已经使用 SIMD 指令(并且带有正确的编译标志)。

确定编译器在做什么的一个通用策略是查看代码的反汇编。如果您不想在自己的机器上执行此操作,可以使用 Godbolt 之类的在线服务:https://gcc.godbolt.org/z/T6GooQ

一个提示是避免atomic 像您在这里所做的那样存储中间结果。原子值用于确保线程之间的同步,相对而言,这可能会带来非常高的计算成本。

【讨论】:

  • 好的。 atomic 是多线程优化留下的。删除它。
【解决方案2】:

查看基于编译器代码的程序集(没有您的 SIMD 内容),

calculations():
        pxor    xmm2, xmm2
        xor     edx, edx
        movdqa  xmm0, XMMWORD PTR .LC0[rip]
        movdqa  xmm11, XMMWORD PTR .LC1[rip]
        movdqa  xmm9, XMMWORD PTR .LC2[rip]
        movdqa  xmm8, XMMWORD PTR .LC3[rip]
        movdqa  xmm7, XMMWORD PTR .LC4[rip]
.L4:
        movdqa  xmm5, xmm0
        movdqa  xmm4, xmm0
        cvtdq2ps        xmm6, xmm0
        movdqa  xmm10, xmm0
        paddd   xmm0, xmm7
        cvtdq2ps        xmm3, xmm0
        paddd   xmm5, xmm9
        paddd   xmm4, xmm8
        cvtdq2ps        xmm5, xmm5
        cvtdq2ps        xmm4, xmm4
        mulps   xmm6, xmm6
        mov     eax, 5120
        paddd   xmm10, xmm11
        mulps   xmm5, xmm5
        mulps   xmm4, xmm4
        mulps   xmm3, xmm3
        pxor    xmm12, xmm12
.L2:
        movdqa  xmm1, xmm12
        cvtdq2ps        xmm14, xmm12
        mulps   xmm14, xmm14
        movdqa  xmm13, xmm12
        paddd   xmm12, xmm7
        cvtdq2ps        xmm12, xmm12
        paddd   xmm1, xmm9
        cvtdq2ps        xmm0, xmm1
        mulps   xmm0, xmm0
        paddd   xmm13, xmm8
        cvtdq2ps        xmm13, xmm13
        sub     eax, 1
        mulps   xmm13, xmm13
        addps   xmm14, xmm6
        mulps   xmm12, xmm12
        addps   xmm0, xmm5
        addps   xmm13, xmm4
        addps   xmm12, xmm3
        addps   xmm0, xmm14
        addps   xmm0, xmm13
        addps   xmm0, xmm12
        movdqa  xmm12, xmm1
        cvttps2dq       xmm0, xmm0
        paddd   xmm2, xmm0
        jne     .L2
        add     edx, 1
        movdqa  xmm0, xmm10
        cmp     edx, 1280
        jne     .L4
        movdqa  xmm0, xmm2
        psrldq  xmm0, 8
        paddd   xmm2, xmm0
        movdqa  xmm0, xmm2
        psrldq  xmm0, 4
        paddd   xmm2, xmm0
        movd    eax, xmm2
        ret
main:
        xor     eax, eax
        ret
_GLOBAL__sub_I_calculations():
        sub     rsp, 8
        mov     edi, OFFSET FLAT:_ZStL8__ioinit
        call    std::ios_base::Init::Init() [complete object constructor]
        mov     edx, OFFSET FLAT:__dso_handle
        mov     esi, OFFSET FLAT:_ZStL8__ioinit
        mov     edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
        add     rsp, 8
        jmp     __cxa_atexit
.LC0:
        .long   0
        .long   1
        .long   2
        .long   3
.LC1:
        .long   4
        .long   4
        .long   4
        .long   4
.LC2:
        .long   1
        .long   1
        .long   1
        .long   1
.LC3:
        .long   2
        .long   2
        .long   2
        .long   2
.LC4:
        .long   3
        .long   3
        .long   3
        .long   3

您的 SIMD 代码生成:

calculations():
        pxor    xmm5, xmm5
        xor     eax, eax
        mov     r8d, 1
        movabs  rdi, -4294967296
        cvtsi2ss        xmm5, eax
.L4:
        mov     r9d, r8d
        mov     esi, 1
        movd    edx, xmm5
        pxor    xmm5, xmm5
        pxor    xmm4, xmm4
        mov     ecx, edx
        mov     rdx, QWORD PTR [rsp-24]
        cvtsi2ss        xmm5, r8d
        add     r8d, 1
        cvtsi2ss        xmm4, r8d
        and     rdx, rdi
        or      rdx, rcx
        pxor    xmm2, xmm2
        mov     edx, edx
        movd    ecx, xmm5
        sal     rcx, 32
        or      rdx, rcx
        mov     QWORD PTR [rsp-24], rdx
        movd    edx, xmm4
        pxor    xmm4, xmm4
        mov     ecx, edx
        mov     rdx, QWORD PTR [rsp-16]
        and     rdx, rdi
        or      rdx, rcx
        lea     ecx, [r9+2]
        mov     edx, edx
        cvtsi2ss        xmm4, ecx
        movd    ecx, xmm4
        sal     rcx, 32
        or      rdx, rcx
        mov     QWORD PTR [rsp-16], rdx
        movaps  xmm4, XMMWORD PTR [rsp-24]
        mulps   xmm4, xmm4
.L2:
        movd    edx, xmm2
        mov     r10d, esi
        pxor    xmm2, xmm2
        pxor    xmm7, xmm7
        mov     ecx, edx
        mov     rdx, QWORD PTR [rsp-40]
        cvtsi2ss        xmm2, esi
        add     esi, 1
        and     rdx, rdi
        cvtsi2ss        xmm7, esi
        or      rdx, rcx
        mov     ecx, edx
        movd    r11d, xmm2
        movd    edx, xmm7
        sal     r11, 32
        or      rcx, r11
        pxor    xmm7, xmm7
        mov     QWORD PTR [rsp-40], rcx
        mov     ecx, edx
        mov     rdx, QWORD PTR [rsp-32]
        and     rdx, rdi
        or      rdx, rcx
        lea     ecx, [r10+2]
        mov     edx, edx
        cvtsi2ss        xmm7, ecx
        movd    ecx, xmm7
        sal     rcx, 32
        or      rdx, rcx
        mov     QWORD PTR [rsp-32], rdx
        movaps  xmm0, XMMWORD PTR [rsp-40]
        mulps   xmm0, xmm0
        addps   xmm0, xmm4
        movaps  xmm3, xmm0
        movaps  xmm1, xmm0
        shufps  xmm3, xmm0, 85
        addss   xmm1, xmm3
        movaps  xmm3, xmm0
        unpckhps        xmm3, xmm0
        shufps  xmm0, xmm0, 255
        addss   xmm1, xmm3
        addss   xmm0, xmm1
        cvttss2si       edx, xmm0
        add     eax, edx
        cmp     r10d, 5120
        jne     .L2
        cmp     r9d, 5120
        jne     .L4
        rep ret
main:
        xor     eax, eax
        ret
_GLOBAL__sub_I_calculations():
        sub     rsp, 8
        mov     edi, OFFSET FLAT:_ZStL8__ioinit
        call    std::ios_base::Init::Init() [complete object constructor]
        mov     edx, OFFSET FLAT:__dso_handle
        mov     esi, OFFSET FLAT:_ZStL8__ioinit
        mov     edi, OFFSET FLAT:_ZNSt8ios_base4InitD1Ev
        add     rsp, 8
        jmp     __cxa_atexit

请注意,编译器的版本使用cvtdq2pspadddcvtdq2psmulpsaddpscvttps2dq。所有这些都是 SIMD 指令。通过有效地组合它们,编译器可以快速生成代码。

相比之下,您的代码会生成很多addandcvtsi2ssleamovmovdorpxorsal,它们是不是 SIMD 指令。

我怀疑编译器在处理数据类型转换和数据重新排列方面做得比你做得更好,这使它能够更有效地安排数学运算。

【讨论】:

  • 所以编译器生成的代码已经过 SIMD 优化,注入 SSE 内部函数不会进一步加速?
  • “这取决于”将是一个公平的答案。在某些情况下,您可以通过自己添加 SIMD 操作来优化性能,但在许多情况下,现代编译器会在不受您干扰的情况下进行出色的优化。
  • @SørenV.Poulsen 好的。我不太擅长汇编,但this 看起来像编译器将:v_x = _mm_mul_ps(v_x, v_x); 翻译成这 5 条指令:movaps xmm0, XMMWORD PTR v_x$[rsp] mulps xmm0, XMMWORD PTR v_x$[rsp] movaps XMMWORD PTR $T8[rsp], xmm0 movaps xmm0, XMMWORD PTR $T8[rsp] movaps XMMWORD PTR v_x$[rsp], xmm0 为什么内存之间移动这么多?有必要吗?还是优化不好?
  • 您必须考虑到 SIMD 操作只能通过在处理器中使用特殊寄存器来启用。将值移入和移出这些寄存器会产生成本,一般来说,数据在寄存器中保存的时间越长越好。仅仅为了几次乘法就将数据移入/移出 SSE 寄存器可能不值得。编译器会为你做这些考虑。
  • @user1554270:如果您使用-O2-O3(甚至可能是-O1)进行编译,编译器将添加SIMD。使用 -march=native 将允许它使用更多的 SIMD。
猜你喜欢
  • 2023-03-09
  • 2020-08-29
  • 2015-11-02
  • 2018-08-16
  • 2013-02-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-02-19
相关资源
最近更新 更多