web-dev-qa-db-ja.com

水平を行う最速の方法SSEベクトルの合計(または他の削減)

3つ(または4つ)の浮動小数点数のベクトルを指定します。それらを合計する最も速い方法は何ですか?

SSE(movaps、shuffle、add、movd))は常にx87より高速ですか?SSE3の水平加算命令はそれだけの価値がありますか?

FPU、次にfaddp、faddpに移行するためのコストはどれくらいですか?最速の特定の命令シーケンスは何ですか?

「一度に4つのベクトルを合計できるように配置する」は、回答として受け入れられません。 :-)例配列を合計するには、垂直合計に複数のベクトルアキュムレータを使用して(addpsレイテンシを非表示にする)、ループ後に1つに減らすことができますが、最後のベクトルを水平に合計する必要があります。

42
FeepingCreature

一般的に、あらゆる種類のベクトルの水平方向の縮小では、抽出/高半分を低にシャッフルし、次に垂直加算(またはmin/max/or/and/xor/multiply/whatever);単一の要素が残るまで繰り返します。128ビットよりも大きいベクトルで開始する場合は、128になるまで半分に狭めます(このベクトルでこの回答の関数の1つを使用できます)。最後にすべての要素に結果をブロードキャストする必要がない限り、全幅シャッフルを行うことを検討できます。

幅の広いベクトルと整数の関連するQ&A:[〜#〜] fp [〜#〜]

整数


this質問への主な回答:ほとんどが浮動小数点数および___m128_

Agner Fogのmicroarchガイド に基づいて調整されたいくつかのバージョンを以下に示します。microarchガイドと指示表。 x86 タグwikiも参照してください。それらは主要なボトルネックがなく、どのCPUでも効率的でなければなりません。 (たとえば、あるuarchでは少し役立つが別のuarchでは遅くなるようなことは避けました)。コードサイズも最小化されます。

一般的なSSE3/SSSE3 2x haddイディオムはコードサイズにのみ有効で、既存のCPUの速度には適していません。これにはユースケース(転置や追加など、以下を参照)がありますが、単一のベクトルはそれらの1つではありません。

AVXバージョンも含まれています。 AVX/AVX2でのあらゆる種類の水平方向の縮小は、_vextractf128_および「垂直」操作で開始して、1つのXMM(___m128_)ベクトルに縮小する必要があります。一般に、幅の広いベクトルの場合、要素の種類に関係なく、128ビットのベクトルになるまで繰り返し半分に狭めるのが最善の策です。 (8ビット整数を除いて、より広い要素にオーバーフローせずにhsumしたい場合の最初のステップとしてvpsadbwを使用します。)

このすべてのコードからのasm出力を表示 Godboltコンパイラエクスプローラーへの私の改良点も参照 Agner FogのC++ベクトルクラスライブラリ _horizontal_add_関数。 ( メッセージボードスレッド 、および github のコード)。 CPPマクロを使用して、SSE2、SSE4、およびAVXのコードサイズに最適なシャッフルを選択し、AVXが使用できない場合にmovdqaを回避しました。


考慮すべきトレードオフがあります。

  • コードサイズ:L1 Iキャッシュの理由、およびディスクからのコードフェッチ(小さいバイナリ)の場合は、小さい方が適切です。バイナリの合計サイズは、プログラム全体で繰り返し行われるコンパイラの決定にとって重要です。組み込み関数を使用して何かを手動でコーディングするのが面倒な場合は、プログラム全体のスピードアップが得られる場合は、数バイトのコードを費やす価値があります(展開のように見えるマイクロベンチマークに注意してください)良い)。
  • uop-cacheサイズ:多くの場合、L1 I $よりも貴重です。 4つのシングルUOP命令は2 haddpsよりもスペースをとらないので、これはここで非常に重要です。
  • 待ち時間:場合によっては
  • スループット(バックエンドポート):通常、無関係な水平合計が最も内側のループの唯一のものではありません。ポート圧力は、これを含むループ全体の一部としてのみ重要です。
  • スループット(フロントエンドの融合ドメインの合計uops):hsumが使用するのと同じポートで周囲のコードがボトルネックにならない場合、これはhsumが全体のスループットに与える影響のプロキシです。

水平方向の追加が少ない場合

CPUuop-cacheを使用しない場合、めったに使用されない場合は2倍のhaddpsが優先される可能性があります。実行すると速度が遅くなりますが、それほど頻繁ではありません。命令が2つだけであるため、周囲のコード(I $サイズ)への影響が最小限に抑えられます。

CPUuop-cacheを使用すると、命令数が多くてもx86コードサイズが大きくても、おそらくuopsが少ないものが優先されます。使用する合計uopsキャッシュラインは最小化したいものであり、合計uopsを最小化するほど単純ではありません(分岐と32B境界は常に新しいuopキャッシュラインを開始します)。

とにかく、とはいえ、水平方向の合計はlotになるので、うまくコンパイルできるバージョンを慎重に作成するための私の試みを次に示します。実際のハードウェアでベンチマークされていないか、注意深くテストされています。シャッフル定数などにバグがある可能性があります。


コードのフォールバック/ベースラインバージョンを作成している場合は、古いCPUのみがそれを実行することに注意してください;新しいCPUでは、AVXバージョン、SSE4.1などを実行します。

K8のような古いCPU、およびCore2(merom)以前には64ビットシャッフルユニットしかありません。 Core2には、ほとんどの命令用に128ビットの実行ユニットがありますが、シャッフル用ではありません。 (Pentium MおよびK8はすべての128bベクトル命令を2つの64ビット半分として処理します)。

movhlpsのような64ビットのチャンクにデータを移動するシャッフル(64ビットの半分内でシャッフルしない)も高速です。

関連:新しいCPUでのシャッフル、およびHaswell以降での1クロックシャッフルスループットのボトルネックを回避するためのトリック: AVX512での128ビットクロスレーン操作により、パフォーマンスが向上しますか?

シャッフルが遅い古いCPUの場合

  • movhlps(Merom:1uop)はshufps(Merom:3uops)より大幅に高速です。 Pentium-Mでは、movapsより安価です。また、Core2のFPドメインで実行され、他のシャッフルによるバイパス遅延を回避します。
  • unpcklpdunpcklpsより高速です。
  • pshufdは遅い、pshuflw/pshufhwは速い(64ビットの半分しかシャッフルしないため)
  • _pshufb mm0_(MMX)は高速で、_pshufb xmm0_は低速です。
  • haddpsは非常に遅い(MeromおよびPentium Mでは6uops)
  • movshdup(メロム:1uop)は興味深いです:64b要素内でシャッフルする唯一の1uop insnです。

Core2(Penrynを含む)のshufpsは、整数ドメインにデータをもたらし、addpsのFP実行ユニットに戻るためのバイパス遅延を引き起こしますが、movhlpsは完全にFPドメインにあります。 shufpdもfloatドメインで実行されます。

movshdupは整数ドメインで実行されますが、uopは1つだけです。

AMD K10、Intel Core2(Penryn/Wolfdale)、およびそれ以降のすべてのCPUは、すべてのxmmシャッフルを単一のuopとして実行します。 (ただし、Penrynではshufpsでのバイパス遅延に注意してください。movhlpsで回避できます)


AVXなしで、無駄なmovaps/movdqa命令を回避するには、シャッフルを慎重に選択する必要があります。宛先を変更するのではなく、少数のシャッフルだけがコピーしてシャッフルとして機能します。 2つの入力(_unpck*_またはmovhlpsなど)からのデータを組み合わせるシャッフルは、_mm_movehl_ps(same,same)の代わりに、不要になったtmp変数で使用できます。

これらのいくつかは、最初のシャッフルの宛先として使用するためのダミー引数を取ることにより、より高速にすることができます(MOVAPSを保存)が、見栄えが悪い/「クリーン」が少なくなります。例:

_// Use dummy = a recently-dead variable that vec depends on,
//  so it doesn't introduce a false dependency,
//  and the compiler probably still has it in a register
__m128d highhalf_pd(__m128d dummy, __m128d vec) {
#ifdef __AVX__
    // With 3-operand AVX instructions, don't create an extra dependency on something we don't need anymore.
    (void)dummy;
    return _mm_unpackhi_pd(vec, vec);
#else
    // Without AVX, we can save a MOVAPS with MOVHLPS into a dead register
    __m128 tmp = _mm_castpd_ps(dummy);
    __m128d high = _mm_castps_pd(_mm_movehl_ps(tmp, _mm_castpd_ps(vec)));
    return high;
#endif
}
_

SSE1(別名SSE):

_float hsum_ps_sse1(__m128 v) {                                  // v = [ D C | B A ]
    __m128 shuf   = _mm_shuffle_ps(v, v, _MM_SHUFFLE(2, 3, 0, 1));  // [ C D | A B ]
    __m128 sums   = _mm_add_ps(v, shuf);      // sums = [ D+C C+D | B+A A+B ]
    shuf          = _mm_movehl_ps(shuf, sums);      //  [   C   D | D+C C+D ]  // let the compiler avoid a mov by reusing shuf
    sums          = _mm_add_ss(sums, shuf);
    return    _mm_cvtss_f32(sums);
}
    # gcc 5.3 -O3:  looks optimal
    movaps  xmm1, xmm0     # I think one movaps is unavoidable, unless we have a 2nd register with known-safe floats in the upper 2 elements
    shufps  xmm1, xmm0, 177
    addps   xmm0, xmm1
    movhlps xmm1, xmm0     # note the reuse of shuf, avoiding a movaps
    addss   xmm0, xmm1

    # clang 3.7.1 -O3:  
    movaps  xmm1, xmm0
    shufps  xmm1, xmm1, 177
    addps   xmm1, xmm0
    movaps  xmm0, xmm1
    shufpd  xmm0, xmm0, 1
    addss   xmm0, xmm1
_

シャッフルの悲観化に関するclangのバグ を報告しました。シャッフルのための独自の内部表現があり、それをシャッフルに戻します。 gccは、使用する組み込み関数に直接一致する命令をより頻繁に使用します。

多くの場合、命令の選択が手動で調整されていないコードでは、clangはgccよりも優れています。または、定数伝播は、組み込み関数が非定数の場合に最適である場合でも、物事を簡略化できます。全体として、コンパイラーはアセンブラーだけでなく、組み込み関数に適したコンパイラーのように機能することは良いことです。多くの場合、コンパイラーはスカラCから良いasmを生成できますが、これは良いasmと同じようには機能しません。最終的にコンパイラは、組み込み関数をオプティマイザへの入力としての別のC演算子として扱います。


SSE3

_float hsum_ps_sse3(__m128 v) {
    __m128 shuf = _mm_movehdup_ps(v);        // broadcast elements 3,1 to 2,0
    __m128 sums = _mm_add_ps(v, shuf);
    shuf        = _mm_movehl_ps(shuf, sums); // high half -> low half
    sums        = _mm_add_ss(sums, shuf);
    return        _mm_cvtss_f32(sums);
}

    # gcc 5.3 -O3: perfectly optimal code
    movshdup    xmm1, xmm0
    addps       xmm0, xmm1
    movhlps     xmm1, xmm0
    addss       xmm0, xmm1
_

これにはいくつかの利点があります。

  • 破壊的なシャッフル(AVXなし)を回避するためにmovapsコピーを必要としません:_movshdup xmm1, xmm2_の宛先は書き込み専用であるため、デッドレジスタからtmpを作成します。これが、movehl_ps(tmp, sums)ではなくmovehl_ps(sums, sums)を使用した理由でもあります。

  • 小さなコードサイズ。シャッフル命令は小さいです:movhlpsは3バイト、movshdupは4バイトです(shufpsと同じ)。即時バイトは必要ないため、AVXではvshufpsは5バイトですが、vmovhlpsvmovshdupは両方とも4です。

addpsの代わりにaddssを使用して別のバイトを節約できます。これは内部ループ内では使用されないため、追加のトランジスタを切り替えるための追加のエネルギーはおそらく無視できます。すべての要素が有効なFPデータを保持しているため、上位3つの要素からのFP例外はリスクになりません。ただし、clang/LLVMは実際にはベクトルシャッフルを「理解」し、低要素のみが重要であることを知っている場合は、より良いコードを生成します。

SSE1バージョンと同様に、奇数要素を追加すると、それ以外では発生しないFP例外(オーバーフローなど)が発生する可能性がありますが、これは問題にはなりません。非正規化は遅いですが、IIRCが+ Infの結果を生成することはほとんどのARCHISHにはありません。


コードサイズを最適化するSSE3

コードサイズが主な懸念事項である場合、2つのhaddps(__mm_hadd_ps_)命令でうまくいきます(ポールRの答え)。これは、入力して覚えるのも最も簡単です。ただし、高速ではないです。 Intel Skylakeでも、各haddpsを6サイクルのレイテンシで3 uopsにデコードします。したがって、マシンコードバイト(L1 Iキャッシュ)を節約しますが、より価値のあるuopキャッシュでより多くのスペースを使用します。 haddpsの実際の使用例: 転置と合計の問題 、または中間ステップでスケーリングを行う このSSE atoi()実装


AVX:

このバージョンでは、コードバイトを節約できます AVX質問に対するMaratの回答

_#ifdef __AVX__
float hsum256_ps_avx(__m256 v) {
    __m128 vlow  = _mm256_castps256_ps128(v);
    __m128 vhigh = _mm256_extractf128_ps(v, 1); // high 128
           vlow  = _mm_add_ps(vlow, vhigh);     // add the low 128
    return hsum_ps_sse3(vlow);         // and inline the sse3 version, which is optimal for AVX
    // (no wasted instructions, and all of them are the 4B minimum)
}
#endif

 vmovaps xmm1,xmm0               # huh, what the heck gcc?  Just extract to xmm1
 vextractf128 xmm0,ymm0,0x1
 vaddps xmm0,xmm1,xmm0
 vmovshdup xmm1,xmm0
 vaddps xmm0,xmm1,xmm0
 vmovhlps xmm1,xmm1,xmm0
 vaddss xmm0,xmm0,xmm1
 vzeroupper 
 ret
_

倍精度:

_double hsum_pd_sse2(__m128d vd) {                      // v = [ B | A ]
    __m128 undef  = _mm_undefined_ps();                       // don't worry, we only use addSD, never touching the garbage bits with an FP add
    __m128 shuftmp= _mm_movehl_ps(undef, _mm_castpd_ps(vd));  // there is no movhlpd
    __m128d shuf  = _mm_castps_pd(shuftmp);
    return  _mm_cvtsd_f64(_mm_add_sd(vd, shuf));
}

# gcc 5.3.0 -O3
    pxor    xmm1, xmm1          # hopefully when inlined, gcc could pick a register it knew wouldn't cause a false dep problem, and avoid the zeroing
    movhlps xmm1, xmm0
    addsd   xmm0, xmm1


# clang 3.7.1 -O3 again doesn't use movhlps:
    xorpd   xmm2, xmm2          # with  #define _mm_undefined_ps _mm_setzero_ps
    movapd  xmm1, xmm0
    unpckhpd        xmm1, xmm2
    addsd   xmm1, xmm0
    movapd  xmm0, xmm1    # another clang bug: wrong choice of operand order


// This doesn't compile the way it's written
double hsum_pd_scalar_sse2(__m128d vd) {
    double tmp;
    _mm_storeh_pd(&tmp, vd);       // store the high half
    double lo = _mm_cvtsd_f64(vd); // cast the low half
    return lo+tmp;
}

    # gcc 5.3 -O3
    haddpd  xmm0, xmm0   # Lower latency but less throughput than storing to memory

    # ICC13
    movhpd    QWORD PTR [-8+rsp], xmm0    # only needs the store port, not the shuffle unit
    addsd     xmm0, QWORD PTR [-8+rsp]
_

メモリに格納して戻すと、ALU uopを回避できます。シャッフルポートの圧力、または一般にALU uopsがボトルネックである場合、これは良いことです。 (x86-64 SysV ABIは、シグナルハンドラーが踏まないレッドゾーンを提供するため、_sub rsp, 8_などは必要ないことに注意してください。)

一部の人々は配列に格納してすべての要素を合計しますが、コンパイラは通常、配列の下位要素が格納前のレジスタにまだ存在することを認識しません。


整数:

pshufdは便利なコピーアンドシャッフルです。残念ながらビットシフトとバイトシフトはインプレースで行われ、punpckhqdqは宛先の上位半分を結果の下位半分に配置します。movhlpsが上位半分を別の半分に抽出する方法とは逆です登録。

最初のステップでmovhlpsを使用することは、一部のCPUでは良いかもしれませんが、スクラッチレジスタがある場合のみです。 pshufdは安全な選択であり、Merom以降はすべて高速です。

_int hsum_epi32_sse2(__m128i x) {
#ifdef __AVX__
    __m128i hi64  = _mm_unpackhi_epi64(x, x);           // 3-operand non-destructive AVX lets us save a byte without needing a mov
#else
    __m128i hi64  = _mm_shuffle_epi32(x, _MM_SHUFFLE(1, 0, 3, 2));
#endif
    __m128i sum64 = _mm_add_epi32(hi64, x);
    __m128i hi32  = _mm_shufflelo_epi16(sum64, _MM_SHUFFLE(1, 0, 3, 2));    // Swap the low two elements
    __m128i sum32 = _mm_add_epi32(sum64, hi32);
    return _mm_cvtsi128_si32(sum32);       // SSE2 movd
    //return _mm_extract_epi32(hl, 0);     // SSE4, even though it compiles to movd instead of a literal pextrd r32,xmm,0
}

    # gcc 5.3 -O3
    pshufd xmm1,xmm0,0x4e
    paddd  xmm0,xmm1
    pshuflw xmm1,xmm0,0x4e
    paddd  xmm0,xmm1
    movd   eax,xmm0

int hsum_epi32_ssse3_slow_smallcode(__m128i x){
    x = _mm_hadd_epi32(x, x);
    x = _mm_hadd_epi32(x, x);
    return _mm_cvtsi128_si32(x);
}
_

一部のCPUでは、整数データに対してFPシャッフルを使用しても安全です。私はこれをしませんでした。なぜなら、最新のCPUでは最大で1または2コードバイトを節約し、速度は向上しません(コードサイズ/整列効果以外)。

71
Peter Cordes

SSE2

4つすべて:

const __m128 t = _mm_add_ps(v, _mm_movehl_ps(v, v));
const __m128 sum = _mm_add_ss(t, _mm_shuffle_ps(t, t, 1));

r1 + r2 + r3:

const __m128 t1 = _mm_movehl_ps(v, v);
const __m128 t2 = _mm_add_ps(v, t1);
const __m128 sum = _mm_add_ss(t1, _mm_shuffle_ps(t2, t2, 1));

これらはdouble HADDPSとほぼ同じ速度であることがわかりました(ただし、あまり厳密に測定していません)。

18
Kornel

SSE3の2つのHADDPS命令で実行できます。

v = _mm_hadd_ps(v, v);
v = _mm_hadd_ps(v, v);

これは、すべての要素に合計を入れます。

10
Paul R

私は間違いなくSSE 4.2を試してみます。これを複数回実行している場合(パフォーマンスに問題がある場合はそうだと思います)、レジスタに(1,1、 1,1)、そしてそれに対していくつかのdot4(my_vec(s)、one_vec)を実行します。はい、それは余分な乗算を行いますが、最近ではかなり安価であり、そのような操作は水平依存関係によって支配されている可能性があります。これは新しいSSEドット積関数でより最適化される可能性があります。PaulRが投稿した2倍の水平加算よりもパフォーマンスが優れているかどうかをテストする必要があります。

また、ストレートスカラー(またはスカラーSSE)コードと比較することをお勧めします-奇妙なことに、通常は高速です(通常、内部的にシリアル化されますが、レジスタバイパスを使用して緊密にパイプライン処理されます。 SIMTのようなコードを実行していますが、そうではないようです(そうしないと、4つのドット積を実行します)。

4
Crowley9