3つ(または4つ)の浮動小数点数のベクトルを指定します。それらを合計する最も速い方法は何ですか?
SSE(movaps、shuffle、add、movd))は常にx87より高速ですか?SSE3の水平加算命令はそれだけの価値がありますか?
FPU、次にfaddp、faddpに移行するためのコストはどれくらいですか?最速の特定の命令シーケンスは何ですか?
「一度に4つのベクトルを合計できるように配置する」は、回答として受け入れられません。 :-)例配列を合計するには、垂直合計に複数のベクトルアキュムレータを使用して(addpsレイテンシを非表示にする)、ループ後に1つに減らすことができますが、最後のベクトルを水平に合計する必要があります。
一般的に、あらゆる種類のベクトルの水平方向の縮小では、抽出/高半分を低にシャッフルし、次に垂直加算(またはmin/max/or/and/xor/multiply/whatever);単一の要素が残るまで繰り返します。128ビットよりも大きいベクトルで開始する場合は、128になるまで半分に狭めます(このベクトルでこの回答の関数の1つを使用できます)。最後にすべての要素に結果をブロードキャストする必要がない限り、全幅シャッフルを行うことを検討できます。
幅の広いベクトルと整数の関連するQ&A:[〜#〜] fp [〜#〜]
__m128
_および___m128d
_この回答(下記を参照)__m256d
_ Ryzen 1とIntelのパフォーマンス分析(_vextractf128
_が_vperm2f128
_よりもはるかに優れている理由を示します) SSE/AVXを使用して__m256dに保存されている値の合計を取得__m256
_ __ m256を水平方向に合計する方法整数
__m128i
_ 32ビット要素:この回答(下記参照)。 64ビットの要素は明らかである必要があります。pshufd/ paddqステップは1つだけです。___m128i
_ 8ビット符号なし要素: psadbw
_mm_setzero_si128()
に対して、2つのqword半分(またはより広いベクトルの場合は4または8)をhsumします。 SSE符号なしバイトベクトルを水平方向に合計する最速の方法 は、SSE2で128ビットを示します。 AVX組み込み関数を使用した__m512iの8ビット整数の合計 には、AVX512の例があります。 SIMDを使用して文字の出現回数をカウントする方法 には、AVX2 ___m256i
_の例があります。
(符号付きバイトの場合、XOR set1(0x80)を使用してSADの前に符号なしにフリップし、最後のhsumからバイアスを差し引くことができます)。
_mm_madd_epi16
_ with set1(1)as a single-uop widening horizontal add building block forarrow integers: SIMD:Accumulate Adjacent Pairs___m256i
_および___m512i
_(32ビット要素)。 AVX512またはAVX2を使用してすべてのパックされた32ビット整数の合計を計算する最速の方法 。 AVX512の場合、インテルは、__mm512_reduce_add_ps
_(およびpd、epi32、epi64)のように、これを行う一連の「ハードウェア命令ではなく」「縮小」インライン関数を追加しました。また、reduce_min/max/mul/and/or。手動で行うと、基本的に同じasmになります。
水平最大値(追加ではなく): SSEを使用した__m128iベクトルの最大値を取得しますか?
__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
を回避しました。
考慮すべきトレードオフがあります。
haddps
よりもスペースをとらないので、これはここで非常に重要です。水平方向の追加が少ない場合:
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ドメインで実行され、他のシャッフルによるバイパス遅延を回避します。unpcklpd
はunpcklps
より高速です。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
}
_
_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演算子として扱います。
_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バイトですが、vmovhlps
とvmovshdup
は両方とも4です。
addps
の代わりにaddss
を使用して別のバイトを節約できます。これは内部ループ内では使用されないため、追加のトランジスタを切り替えるための追加のエネルギーはおそらく無視できます。すべての要素が有効なFPデータを保持しているため、上位3つの要素からのFP例外はリスクになりません。ただし、clang/LLVMは実際にはベクトルシャッフルを「理解」し、低要素のみが重要であることを知っている場合は、より良いコードを生成します。
SSE1バージョンと同様に、奇数要素を追加すると、それ以外では発生しないFP例外(オーバーフローなど)が発生する可能性がありますが、これは問題にはなりません。非正規化は遅いですが、IIRCが+ Infの結果を生成することはほとんどのARCHISHにはありません。
コードサイズが主な懸念事項である場合、2つのhaddps
(__mm_hadd_ps
_)命令でうまくいきます(ポールRの答え)。これは、入力して覚えるのも最も簡単です。ただし、高速ではないです。 Intel Skylakeでも、各haddps
を6サイクルのレイテンシで3 uopsにデコードします。したがって、マシンコードバイト(L1 Iキャッシュ)を節約しますが、より価値のあるuopキャッシュでより多くのスペースを使用します。 haddps
の実際の使用例: 転置と合計の問題 、または中間ステップでスケーリングを行う このSSE atoi()
実装 。
このバージョンでは、コードバイトを節約できます 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コードバイトを節約し、速度は向上しません(コードサイズ/整列効果以外)。
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));
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
とほぼ同じ速度であることがわかりました(ただし、あまり厳密に測定していません)。
SSE3の2つのHADDPS
命令で実行できます。
v = _mm_hadd_ps(v, v);
v = _mm_hadd_ps(v, v);
これは、すべての要素に合計を入れます。
私は間違いなくSSE 4.2を試してみます。これを複数回実行している場合(パフォーマンスに問題がある場合はそうだと思います)、レジスタに(1,1、 1,1)、そしてそれに対していくつかのdot4(my_vec(s)、one_vec)を実行します。はい、それは余分な乗算を行いますが、最近ではかなり安価であり、そのような操作は水平依存関係によって支配されている可能性があります。これは新しいSSEドット積関数でより最適化される可能性があります。PaulRが投稿した2倍の水平加算よりもパフォーマンスが優れているかどうかをテストする必要があります。
また、ストレートスカラー(またはスカラーSSE)コードと比較することをお勧めします-奇妙なことに、通常は高速です(通常、内部的にシリアル化されますが、レジスタバイパスを使用して緊密にパイプライン処理されます。 SIMTのようなコードを実行していますが、そうではないようです(そうしないと、4つのドット積を実行します)。