web-dev-qa-db-ja.com

コンパイラは通常のCコードにSSE命令を使用しますか?

デフォルトで-msse -msse2 -mfpmath=sseフラグを使用している人は、これによってパフォーマンスが向上することを期待しています。 SSEは、Cコードで特別なベクトル型が使用されると関与することを知っています。しかし、これらのフラグは通常のCコードに違いをもたらしますか?コンパイラーはSSE =通常のCコードを最適化するには?

9
Jennifer M.

はい。完全に最適化してコンパイルすると、最新のコンパイラはSSE2で自動ベクトル化されます。 clangは-O2でも、gccは-O3でも有効にします。

-O1または-Osの場合でも、コンパイラーはSIMDロード/ストア命令を使用して、整数レジスターよりも幅の広い構造体またはその他のオブジェクトをコピーまたは初期化します。これは実際には自動ベクトル化としてカウントされません。これは、小さな固定サイズのブロックに対するデフォルトの組み込みmemset/memcpy戦略の一部に似ています。ただし、SIMD命令を利用し、サポートする必要があります。


SSE2はx86-64のベースライン/オプションではないため、コンパイラーはx86-64をターゲットにするときに常にSSE1/SSE2命令を使用できます。それ以降の命令セット(SSE4、AVX、AVX2、AVX512、およびBMI2、popcntなどの非SIMD拡張命令)を手動で有効にして、古いCPUで実行されないコードを作成してもよいことをコンパイラーに通知する必要があります。または、コードの複数のバージョンを生成して実行時に選択するように取得しますが、これには余分なオーバーヘッドがあり、より大きな関数の場合にのみ価値があります。

_-msse -msse2 -mfpmath=sse_はすでにx86-64のデフォルトですが、32ビットi386のデフォルトではありません。一部の32ビット呼び出し規約はx87レジスタでFP値を返すため、計算にSSE/SSE2を使用してから、結果を保存/再ロードしてx87で取得する必要がある場合がありますst(0)。_-mfpmath=sse_を使用すると、よりスマートなコンパイラーは、FP戻り値を生成する計算にx87を使用する可能性があります。

32ビットx86では、_-msse2_がデフォルトでオンになっていない可能性があります。これは、コンパイラの構成方法によって異なります。非常に古いCPUをターゲットにしているために32ビットを使用している場合64ビットコードを実行できない場合は、無効になっていることを確認するか、 _-msse_のみ。

コンパイルしているCPUに合わせてバイナリを調整する最良の方法は_-O3 -march=native -mfpmath=sse_であり、リンク時間最適化+プロファイルガイド最適化。 (gcc _-fprofile-generate_ /いくつかのテストデータで実行/ _gcc -fprofile-use_)。

_-march=native_を使用すると、コンパイラが新しい命令を使用することを選択した場合、以前のCPUでは実行されない可能性のあるバイナリが作成されます。プロファイルに基づく最適化はgccにとって非常に役立ちます。それなしではループを展開することはありません。しかし、PGOを使用すると、どのループが頻繁に/多くの反復で実行されるか、つまり、どのループが「ホット」であり、より多くのコードサイズを費やす価値があるかがわかります。リンク時の最適化により、ファイル間でのインライン化/定数伝播が可能になります。ヘッダーファイルで実際に定義していない小さな関数がたくさんあるC++がある場合は、非常に便利です。


コンパイラ出力の確認と意味の理解については、 GCC/clangアセンブリ出力から「ノイズ」を削除する方法を参照してください。それ。

ここにいくつかの特定の例があります Godboltコンパイラエクスプローラーで x86-64の場合。 Godboltには他のいくつかのアーキテクチャ用のgccもあり、clangを使用すると、_-target mips_などを追加できるため、ARM NEONの自動ベクトル化と、有効にする適切なコンパイラオプションを確認できます。 x86-64コンパイラで_-m32_を使用して、32ビットのコード生成を取得できます。

_int sumint(int *arr) {
    int sum = 0;
    for (int i=0 ; i<2048 ; i++){
        sum += arr[i];
    }
    return sum;
}
_

_gcc8.1 -O3_を含む内部ループ(_-march=haswell_またはAVX/AVX2を有効にするものを含まない):

_.L2:                                 # do {
    movdqu  xmm2, XMMWORD PTR [rdi]    # load 16 bytes
    add     rdi, 16
    paddd   xmm0, xmm2                 # packed add of 4 x 32-bit integers
    cmp     rax, rdi
    jne     .L2                      # } while(p != endp)

    # then horizontal add and extract a single 32-bit sum
_

_-ffast-math_がないと、コンパイラーはFP操作を並べ替えることができないため、同等のfloatは自動ベクトル化されません(Godboltリンクを参照してください:スカラーaddssを取得します)。ループごとに、または_-ffast-math_を使用します。

ただし、一部のFPのものは、操作の順序を変更せずに安全に自動ベクトル化できます。

_// clang won't contract this into an FMA without -ffast-math :/
// but gcc will (if you compile with -march=haswell)
void scale_array(float *arr) {
    for (int i=0 ; i<2048 ; i++){
        arr[i] = arr[i] * 2.1f + 1.234f;
    }
}

  # load constants: xmm2 = {2.1,  2.1,  2.1,  2.1}
  #                 xmm1 = (1.23, 1.23, 1.23, 1.23}
.L9:   # gcc8.1 -O3                       # do {
    movups  xmm0, XMMWORD PTR [rdi]         # load unaligned packed floats
    add     rdi, 16
    mulps   xmm0, xmm2                      # multiply Packed Single-precision
    addps   xmm0, xmm1                      # add Packed Single-precision
    movups  XMMWORD PTR [rdi-16], xmm0      # store back to the array
    cmp     rax, rdi
    jne     .L9                           # }while(p != endp)
_

乗数= _2.0f_の結果、addpsが2倍になり、Haswell/Broadwellのスループットが2分の1に削減されます。 SKLの前は、FP addは1つの実行ポートでのみ実行されますが、乗算を実行できるFMAユニットは2つあります。SKLは加算器を削除し、クロックあたり2のスループットとレイテンシでaddを実行します。 mulとFMA。( http://agner.org/optimize/ 、および x86タグwiki の他のパフォーマンスリンクを参照してください。)

_-march=haswell_を使用してコンパイルすると、コンパイラーはスケール+追加に単一のFMAを使用できます。 (ただし、_-ffast-math_を使用しない限り、clangは式をFMAに縮小しません。IIRCには、他の積極的な操作なしでFP縮小を有効にするオプションがあります。)

10
Peter Cordes