2017-10-16 8 views
0

SSEを使用しない場合よりもSSEが高速になると予想します。追加のコンパイラフラグを追加する必要がありますか?これは浮動小数点ではなく整数コードなので、スピードアップが見えないのでしょうか?SSE:_mm_add_epi32を使用してスピードアップが表示されない

呼び出し/出力

$ make sum2 
clang -O3 -msse -msse2 -msse3 -msse4.1 sum2.c ; ./a.out 123 
n: 123 
    SSE Time taken: 0 seconds 124 milliseconds 
vector+vector:begin int: 1 5 127 0 
vector+vector:end int: 0 64 66 68 
NOSSE Time taken: 0 seconds 115 milliseconds 
vector+vector:begin int: 1 5 127 0 
vector+vector:end int: 0 64 66 68 

コンパイラ

$ clang --version 
Apple LLVM version 9.0.0 (clang-900.0.37) 
Target: x86_64-apple-darwin16.7.0 
Thread model: posix 

sum2.c

#include <stdlib.h> 
#include <stdio.h> 
#include <x86intrin.h> 
#include <time.h> 
#ifndef __cplusplus 
#include <stdalign.h> // C11 defines _Alignas(). This header defines alignas() 
#endif 
#define CYCLE_COUNT 10000 

// add vector and return resulting value on stack 
__attribute__((noinline)) __m128i add_iv(__m128i *a, __m128i *b) { 
    return _mm_add_epi32(*a,*b); 
} 

// add int vectors via sse 
__attribute__((noinline)) void add_iv_sse(__m128i *a, __m128i *b, __m128i *out, int N) { 
    for(int i=0; i<N/sizeof(int); i++) { 
     //out[i]= _mm_add_epi32(a[i], b[i]); // this also works 
     _mm_storeu_si128(&out[i], _mm_add_epi32(a[i], b[i])); 
    } 
} 

// add int vectors without sse 
__attribute__((noinline)) void add_iv_nosse(int *a, int *b, int *out, int N) { 
    for(int i=0; i<N; i++) { 
     out[i] = a[i] + b[i]; 
    } 
} 

__attribute__((noinline)) void p128_as_int(__m128i in) { 
    alignas(16) uint32_t v[4]; 
    _mm_store_si128((__m128i*)v, in); 
    printf("int: %i %i %i %i\n", v[0], v[1], v[2], v[3]); 
} 

// print first 4 and last 4 elements of int array 
__attribute__((noinline)) void debug_print(int *h) { 
    printf("vector+vector:begin "); 
    p128_as_int(* (__m128i*) &h[0]); 
    printf("vector+vector:end "); 
    p128_as_int(* (__m128i*) &h[32764]); 
} 

int main(int argc, char *argv[]) { 
    int n = atoi (argv[1]); 
    printf("n: %d\n", n); 
    // sum: vector + vector, of equal length 
    int f[32768] __attribute__((aligned(16))) = {0,2,4}; 
    int g[32768] __attribute__((aligned(16))) = {1,3,n}; 
    int h[32768] __attribute__((aligned(16))); 
    f[32765] = 33; f[32766] = 34; f[32767] = 35; 
    g[32765] = 31; g[32766] = 32; g[32767] = 33; 

    // https://stackoverflow.com/questions/459691/best-timing-method-in-c 
    clock_t start = clock(); 
     for(int i=0; i<CYCLE_COUNT; ++i) { 
      add_iv_sse((__m128i*)f, (__m128i*)g, (__m128i*)h, 32768); 
     } 
    int msec = (clock()-start) * 1000/CLOCKS_PER_SEC; 
    printf(" SSE Time taken: %d seconds %d milliseconds\n", msec/1000, msec%1000); 
    debug_print(h); 

    // process intense function again 
    start = clock(); 
     for(int i=0; i<CYCLE_COUNT; ++i) { 
      add_iv_nosse(f, g, h, 32768); 
     } 
    msec = (clock()-start) * 1000/CLOCKS_PER_SEC; 
    printf("NOSSE Time taken: %d seconds %d milliseconds\n", msec/1000, msec%1000); 
    debug_print(h); 

    return EXIT_SUCCESS; 
} 
+0

また、幸運にも決して使うことのない 'add_iv'に対するあなたのコメントは間違っています:' __m128i'戻り値は、スタック上ではなくx86-64 System V呼び出し規約のXMM0に返されます。 –

+0

ありがとうピーター!コンパイラが特定のブロックでSSE命令を使用しないようにする方法はありますか? – AG1

+0

私は自動ベクトル化されたコードと手作業のベクトル化されたループのいくつかのperf分析を使って答えを更新しました。どちらもオーバーヘッドが多いですが、4kエイリアシングが帯域幅を傷つけない限り、マニュアルは速くなければならないと思います。だから多分ターボエフェクトは、より多くのCPUサイクルを要しても、第2ループのウォールクロック時間を短くするか、あるいは別の効果があるかもしれません。 –

答えて

4

ASMを見てみましょう。打ち鳴らします-O2または-O3はおそらくadd_iv_nosseを自動ベクトル化します(int * restrict aなどを使用しなかったため、オーバーラップをチェックします)。

-fno-tree-vectorizeを使用すると、組み込み関数の使用を停止することなく、自動ベクトル化が無効になります。私はclang -march=native -mno-avx -O3 -fno-tree-vectorizeをテストしたいと思うもの、スカラー整数とレガシーSSE padddをテストすることをお勧めします。これはgccとclangで動作します。clangではAFAIKはclang特有の-fno-vectorizeの同義語です。

ところで、CPUがフルターボになっていないため、同じ実行可能ファイルの両方でタイミングが崩れる直ちに。 CPUがフルスピードに達する前に、おそらくコードのタイムアウトセクションに入っているでしょう。 (for i in {1..10}; do time ./a.out; donefor i in {1..10}; do time ./a.out; doneでこれを数回実行してください。

Linuxの場合、私はperf stat -r5 ./a.outを使用してパフォーマンスカウンターで5回実行します。その他、私は全体の実行のためPERFカウンターで見ることができる)


コードレビュー:。

あなたはuint32_tためstdint.hを忘れてしまった私はcompile on Godbolt to see the asmにそれを得るためにそれを追加する必要がありました(clang-を仮定。 5.0はあなたが使用しているApple clangバージョンのようなものです。Appleのclangが広告を暗示している場合はIDK efault -mtune=オプションがありますが、それはMacをターゲットにしているだけなので意味があります。また、ベースラインのSSSE3は、x86-64 OS X上で64ビットにも意味があります)

debug_printにはnoinlineは必要ありません。また、別の名前のCYCLE_COUNTをお勧めします。この文脈のサイクルは私にクロックサイクルを考えるので、それをREP_COUNTまたはREPEATSなどと呼んでください。

mainのスタックに配列を置くと、おそらく問題ありません。両方の入力配列を初期化します(ほとんど0になりますが、パフォーマンスはデータに依存しません)。

初期化されていないままにしておくと、各アレイの複数の4kページが同一の物理ゼロページにコピーオンライトマッピングされている可能性があるため、L1Dキャッシュヒットの予想数を超えることがあります。

SSE2ループは、4 * 32kiB * 3 = 384 kiBに設定されているため、L2/L3キャッシュ帯域幅でボトルネックになるため、Intel CPUでは256キロバイトL2キャッシュの約1.5倍です。

clangが手動組み込みループよりも自動ベクトル化ループを展開する可能性があります。これは、2つの負荷+クロックが1つのストアを取得していない場合、16Bのベクトル(32B AVX2ではなく)だけがキャッシュ帯域幅を飽和させないため、パフォーマンスが向上する可能性があります。

更新:実際には、ループオーバーヘッドは3つのポインタ増分+ループカウンタで2つだけアンローされ、それを償却するのはかなり極端です。


自動ベクトル化されたループ:

.LBB2_12:        # =>This Inner Loop Header: Depth=1 
    movdqu xmm0, xmmword ptr [r9 - 16] 
    movdqu xmm1, xmmword ptr [r9]   # hoisted load for 2nd unrolled iter 
    movdqu xmm2, xmmword ptr [r10 - 16] 
    paddd xmm2, xmm0 
    movdqu xmm0, xmmword ptr [r10] 
    paddd xmm0, xmm1 
    movdqu xmmword ptr [r11 - 16], xmm2 
    movdqu xmmword ptr [r11], xmm0 
    add  r9, 32 
    add  r10, 32 
    add  r11, 32 
    add  rbx, -8    # add/jne macro-fused on SnB-family CPUs 
    jne  .LBB2_12 

だから、12融合ドメインのuopだし、せいぜい4つのuopのフロントエンドの問題帯域幅にボトルネック3つのクロック、あたり2ベクトルを実行することができますクロックごとに。

アラインメントが既知のmainにインライン展開せずにコンパイラにその情報がないため、アライメントがp = __builtin_assume_aligned(p, 16)またはスタンドアロン機能のものと保証されていないため、アライメントされたロードを使用していません。整列された負荷(またはAVX)は、別のmovdquロードの代わりにpadddにメモリオペランドを使用させます。

手作業でベクトル化されたループは、整列されたロードを使用してフロントエンドのuopを保存しますが、ループカウンタからのループオーバーヘッドが多くなります。

.LBB1_7:        # =>This Inner Loop Header: Depth=1 
    movdqa xmm0, xmmword ptr [rcx - 16] 
    paddd xmm0, xmmword ptr [rax - 16] 
    movdqu xmmword ptr [r11 - 16], xmm0 

    movdqa xmm0, xmmword ptr [rcx] 
    paddd xmm0, xmmword ptr [rax] 
    movdqu xmmword ptr [r11], xmm0 

    add  r10, 2    # separate loop counter 
    add  r11, 32    # 3 pointer incrmeents 
    add  rax, 32 
    add  rcx, 32 
    cmp  r9, r10    # compare the loop counter 
    jne  .LBB1_7 

したがって、11個の融合ドメインのuopsです。自動ベクトル化されたループよりも速く実行する必要があります。あなたのタイミング方法はおそらく問題を引き起こしました。

(自動ベクトル化されたループでは4つのロードと2つのストアが実行されましたが実際には説明がありますが配列は4kiBの倍数であり、相対的な位置合わせ。あなたはCPUがストアが負荷と重ならないことを確認していないことを意味しており、ここでは4Kエイリアシングを取得される可能性がありますので。私はあなたがそれをチェックすることができるパフォーマンスカウンタがあると思う。)


Agner Fog's microarch guide (and instruction tables + optimization guideタグwikiの他のリンク、特にインテルの最適化ガイドを参照してください。

タグwikiには、優れたSSE/SIMD初心者用のものもあります。

+0

Clangと私はたいてい '-fno-vectorize'を使います。なぜ '-fno-tree-vectorize'を使うのですか?(GCCとの一貫性を除いて) –

+0

@ Zboson:私はclangがそのオプションのために別の名前を持っていたことを、感謝した。 –

関連する問題