簡體   English   中英

SSE內在函數和循環展開

[英]SSE Intrinsics and 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;
}

這是做什么的? 如果循環不能被您選擇展開的因子分割,是否只是為了做額外的部分? 謝謝。

答案是第一個塊:

    __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_UNROLL0.994秒,編譯器沒有選擇SSE
  • UNROLL :3.511秒 ,使用movups
  • SSE_UNROLL3.315秒,使用movups
  • SSE_UNROLL_ALIGNED3.276秒,使用movaps

很明顯,在這種情況下展開循環並沒有幫助。 即使確保我們使用更高效的movaps也無濟於事。

但是當編譯為64位(x64)時,我得到了一個更奇怪的結果:

  • NO_UNROLL1.138秒,編譯器沒有選擇SSE
  • UNROLL1.409秒,編譯器沒有選擇SSE
  • SSE_UNROLL1.420秒, 編譯器仍然沒有選擇SSE!
  • SSE_UNROLL_ALIGNED1.476秒, 編譯器仍然沒有選擇SSE!

似乎MSVC通過提案看到了並且產生了更好的組裝,盡管仍然比我們根本沒有嘗試任何手動優化要慢。

像往常一樣,展開循環並嘗試手動匹配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

循環也可以展開,它只會產生更長的代碼,我不想在這里使用。 你可以相信我 - 編譯器會展開循環。

結論

手動展開對你沒有好處。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM