SSE内在函数和循环展开

时间:2016-03-10 16:04:15

标签: c++ optimization sse loop-unrolling

我正在尝试优化一些循环而且我已经管理但我想知道我是否只是部分纠正了它。比方说我有这个循环:

for(i=0;i<n;i++){
b[i] = a[i]*2;
}

将其展开3倍,产生:

int unroll = (n/4)*4;
for(i=0;i<unroll;i+=4)
{
b[i] = a[i]*2;
b[i+1] = a[i+1]*2;
b[i+2] = a[i+2]*2;
b[i+3] = a[i+3]*2;
}
for(;i<n;i++)
{
b[i] = a[i]*2;
} 

现在是SSE翻译等价物:

__m128 ai_v = _mm_loadu_ps(&a[i]);
__m128 two_v = _mm_set1_ps(2);
__m128 ai2_v = _mm_mul_ps(ai_v, two_v);
_mm_storeu_ps(&b[i], ai2_v);

或是它:

__m128 ai_v = _mm_loadu_ps(&a[i]);
__m128 two_v = _mm_set1_ps(2);
__m128 ai2_v = _mm_mul_ps(ai_v, two_v);
_mm_storeu_ps(&b[i], ai2_v);

__m128 ai1_v = _mm_loadu_ps(&a[i+1]);
__m128 two1_v = _mm_set1_ps(2);
__m128 ai_1_2_v = _mm_mul_ps(ai1_v, two1_v);
_mm_storeu_ps(&b[i+1], ai_1_2_v);

__m128 ai2_v = _mm_loadu_ps(&a[i+2]);
__m128 two2_v = _mm_set1_ps(2);
__m128 ai_2_2_v = _mm_mul_ps(ai2_v, two2_v);
_mm_storeu_ps(&b[i+2], ai_2_2_v);

__m128 ai3_v = _mm_loadu_ps(&a[i+3]);
__m128 two3_v = _mm_set1_ps(2);
__m128 ai_3_2_v = _mm_mul_ps(ai3_v, two3_v);
_mm_storeu_ps(&b[i+3], ai_3_2_v);

我对代码部分感到有些困惑:

for(;i<n;i++)
{
b[i] = a[i]*2;
}
这是做什么的?如果循环不能被您选择展开的因子分割,是否只是为了做额外的部分?谢谢。

2 个答案:

答案 0 :(得分:4)

答案是第一个块:

    __m128 ai_v = _mm_loadu_ps(&a[i]);
    __m128 two_v = _mm_set1_ps(2);
    __m128 ai2_v = _mm_mul_ps(ai_v,two_v);
    _mm_storeu_ps(&b[i],ai2_v);

一次只需要四个变量。

以下是完整的程序,其代码部分已注释掉:

#include <iostream>

int main()
{
    int i{0};
    float a[10] ={1,2,3,4,5,6,7,8,9,10};
    float b[10] ={0,0,0,0,0,0,0,0,0,0};

    int n = 10;
    int unroll = (n/4)*4;
    for (i=0; i<unroll; i+=4) {
        //b[i] = a[i]*2;
        //b[i+1] = a[i+1]*2;
        //b[i+2] = a[i+2]*2;
        //b[i+3] = a[i+3]*2;
        __m128 ai_v = _mm_loadu_ps(&a[i]);
        __m128 two_v = _mm_set1_ps(2);
        __m128 ai2_v = _mm_mul_ps(ai_v,two_v);
        _mm_storeu_ps(&b[i],ai2_v);
    }

    for (; i<n; i++) {
        b[i] = a[i]*2;
    }

    for (auto i : a) { std::cout << i << "\t"; }
    std::cout << "\n";
    for (auto i : b) { std::cout << i << "\t"; }
    std::cout << "\n";

    return 0;
}

效率;似乎我的系统上的程序集生成movups指令,而手动代码可以使用movaps,这应该更快。

我使用以下程序做一些基准测试:

#include <iostream>
//#define NO_UNROLL
//#define UNROLL
//#define SSE_UNROLL
#define SSE_UNROLL_ALIGNED

int main()
{
    const size_t array_size = 100003;
#ifdef SSE_UNROLL_ALIGNED
    __declspec(align(16)) int i{0};
    __declspec(align(16)) float a[array_size] ={1,2,3,4,5,6,7,8,9,10};
    __declspec(align(16)) float b[array_size] ={0,0,0,0,0,0,0,0,0,0};
#endif
#ifndef SSE_UNROLL_ALIGNED
    int i{0};
    float a[array_size] ={1,2,3,4,5,6,7,8,9,10};
    float b[array_size] ={0,0,0,0,0,0,0,0,0,0};
#endif

    int n = array_size;
    int unroll = (n/4)*4;


    for (size_t j{0}; j < 100000; ++j) {
#ifdef NO_UNROLL
        for (i=0; i<n; i++) {
            b[i] = a[i]*2;
        }
#endif
#ifdef UNROLL
        for (i=0; i<unroll; i+=4) {
            b[i] = a[i]*2;
            b[i+1] = a[i+1]*2;
            b[i+2] = a[i+2]*2;
            b[i+3] = a[i+3]*2;
        }
#endif
#ifdef SSE_UNROLL
        for (i=0; i<unroll; i+=4) {
            __m128 ai_v = _mm_loadu_ps(&a[i]);
            __m128 two_v = _mm_set1_ps(2);
            __m128 ai2_v = _mm_mul_ps(ai_v,two_v);
            _mm_storeu_ps(&b[i],ai2_v);
        }
#endif
#ifdef SSE_UNROLL_ALIGNED
        for (i=0; i<unroll; i+=4) {
            __m128 ai_v = _mm_load_ps(&a[i]);
            __m128 two_v = _mm_set1_ps(2);
            __m128 ai2_v = _mm_mul_ps(ai_v,two_v);
            _mm_store_ps(&b[i],ai2_v);
        }
#endif
#ifndef NO_UNROLL
        for (; i<n; i++) {
            b[i] = a[i]*2;
        }
#endif
    }

    //for (auto i : a) { std::cout << i << "\t"; }
    //std::cout << "\n";
    //for (auto i : b) { std::cout << i << "\t"; }
    //std::cout << "\n";

    return 0;
}

我得到了以下结果(x86):

  • NO_UNROLL 0.994 秒,编译器未选择SSE
  • UNROLL 3.511 秒,使用movups
  • SSE_UNROLL 3.315 秒,使用movups
  • SSE_UNROLL_ALIGNED 3.276 秒,使用movaps

很明显,在这种情况下展开循环并没有帮助。即使确保我们使用效率更高的movaps也无济于事。

但是在编译为64位(x64)时,我得到了一个更奇怪的结果:

  • NO_UNROLL 1.138 秒,编译器未选择SSE
  • UNROLL 1.409 秒,编译器未选择SSE
  • SSE_UNROLL 1.420 秒,编译器仍未选择SSE!
  • SSE_UNROLL_ALIGNED 1.476 秒,编译器仍未选择SSE!

似乎MSVC通过提案看到并产生了更好的组装,尽管仍然比我们根本没有尝试任何手动优化要慢。

答案 1 :(得分:2)

像往常一样,展开循环并尝试手动匹配SSE指令效率不高。编译器可以比你做得更好。例如,提供的示例将自动编译为启用SSE的ASM:

foo:
.LFB0:
    .cfi_startproc
    testl   %edi, %edi
    jle .L7
    movl    %edi, %esi
    shrl    $2, %esi
    cmpl    $3, %edi
    leal    0(,%rsi,4), %eax
    jbe .L8
    testl   %eax, %eax
    je  .L8
    vmovdqa .LC0(%rip), %xmm1
    xorl    %edx, %edx
    xorl    %ecx, %ecx
    .p2align 4,,10
    .p2align 3
.L6:
    addl    $1, %ecx
    vpmulld a(%rdx), %xmm1, %xmm0
    vmovdqa %xmm0, b(%rdx)
    addq    $16, %rdx
    cmpl    %esi, %ecx
    jb  .L6
    cmpl    %eax, %edi
    je  .L7
    .p2align 4,,10
    .p2align 3
.L9:
    movslq  %eax, %rdx
    addl    $1, %eax
    movl    a(,%rdx,4), %ecx
    addl    %ecx, %ecx
    cmpl    %eax, %edi
    movl    %ecx, b(,%rdx,4)
    jg  .L9
.L7:
    rep
    ret
.L8:
    xorl    %eax, %eax
    jmp .L9
    .cfi_endproc

循环也可以展开,它只会产生更长的代码,我不想在这里使用。你可以相信我 - 编译器会展开循环。

结论

手动展开对你没有好处。