私は命令最適化の初心者です。
2つのfloat配列の内積を取得するために使用される単純な関数dotpで簡単な分析を行いました。
Cコードは次のとおりです。
float dotp(
const float x[],
const float y[],
const short n
)
{
short i;
float suma;
suma = 0.0f;
for(i=0; i<n; i++)
{
suma += x[i] * y[i];
}
return suma;
}
ウェブ上でAgnerFogが提供するテストフレームを使用しています testp 。
この場合に使用される配列は整列されます:
int n = 2048;
float* z2 = (float*)_mm_malloc(sizeof(float)*n, 64);
char *mem = (char*)_mm_malloc(1<<18,4096);
char *a = mem;
char *b = a+n*sizeof(float);
char *c = b+n*sizeof(float);
float *x = (float*)a;
float *y = (float*)b;
float *z = (float*)c;
次に、関数dotp、n = 2048、repeat = 100000を呼び出します。
for (i = 0; i < repeat; i++)
{
sum = dotp(x,y,n);
}
コンパイルオプション-O3を指定してgcc4.8.3でコンパイルします。
FMA命令をサポートしていないコンピューターでこのアプリケーションをコンパイルしているので、SSE命令しかないことがわかります。
アセンブリコード:
.L13:
movss xmm1, DWORD PTR [rdi+rax*4]
mulss xmm1, DWORD PTR [rsi+rax*4]
add rax, 1
cmp cx, ax
addss xmm0, xmm1
jg .L13
私はいくつかの分析を行います:
μops-fused la 0 1 2 3 4 5 6 7
movss 1 3 0.5 0.5
mulss 1 5 0.5 0.5 0.5 0.5
add 1 1 0.25 0.25 0.25 0.25
cmp 1 1 0.25 0.25 0.25 0.25
addss 1 3 1
jg 1 1 1 -----------------------------------------------------------------------------
total 6 5 1 2 1 1 0.5 1.5
実行後、次の結果が得られます。
Clock | Core cyc | Instruct | BrTaken | uop p0 | uop p1
--------------------------------------------------------------------
542177906 |609942404 |1230100389 |205000027 |261069369 |205511063
--------------------------------------------------------------------
2.64 | 2.97 | 6.00 | 1 | 1.27 | 1.00
uop p2 | uop p3 | uop p4 | uop p5 | uop p6 | uop p7
-----------------------------------------------------------------------
205185258 | 205188997 | 100833 | 245370353 | 313581694 | 844
-----------------------------------------------------------------------
1.00 | 1.00 | 0.00 | 1.19 | 1.52 | 0.00
2行目は、Intelレジスタから読み取られた値です。 3行目は、支店番号「BrTaken」で除算されます。
したがって、ループには、分析と一致する6つの命令、7つのuopsがあることがわかります。
ポート0ポート1ポート5ポート6で実行されるuopsの数は、分析の結果と同様です。おそらくuopsスケジューラがこれを行うと思います、それはポートの負荷を分散しようとするかもしれません、私は正しいですか?
ループごとに約3サイクルしかない理由がわかりません。 Agnerの 命令テーブル によると、命令mulss
のレイテンシーは5であり、ループ間に依存関係があります。私が見る限り、ループごとに少なくとも5サイクルかかるはずです。
誰かがいくつかの洞察を流すことができますか?
================================================== ================
この関数の最適化されたバージョンをnasmで書き、ループを8倍に展開し、vfmadd231ps
命令を使用してみました。
.L2:
vmovaps ymm1, [rdi+rax]
vfmadd231ps ymm0, ymm1, [rsi+rax]
vmovaps ymm2, [rdi+rax+32]
vfmadd231ps ymm3, ymm2, [rsi+rax+32]
vmovaps ymm4, [rdi+rax+64]
vfmadd231ps ymm5, ymm4, [rsi+rax+64]
vmovaps ymm6, [rdi+rax+96]
vfmadd231ps ymm7, ymm6, [rsi+rax+96]
vmovaps ymm8, [rdi+rax+128]
vfmadd231ps ymm9, ymm8, [rsi+rax+128]
vmovaps ymm10, [rdi+rax+160]
vfmadd231ps ymm11, ymm10, [rsi+rax+160]
vmovaps ymm12, [rdi+rax+192]
vfmadd231ps ymm13, ymm12, [rsi+rax+192]
vmovaps ymm14, [rdi+rax+224]
vfmadd231ps ymm15, ymm14, [rsi+rax+224]
add rax, 256
jne .L2
結果:
Clock | Core cyc | Instruct | BrTaken | uop p0 | uop p1
------------------------------------------------------------------------
24371315 | 27477805| 59400061 | 3200001 | 14679543 | 11011601
------------------------------------------------------------------------
7.62 | 8.59 | 18.56 | 1 | 4.59 | 3.44
uop p2 | uop p3 | uop p4 | uop p5 | uop p6 | uop p7
-------------------------------------------------------------------------
25960380 |26000252 | 47 | 537 | 3301043 | 10
------------------------------------------------------------------------------
8.11 |8.13 | 0.00 | 0.00 | 1.03 | 0.00
したがって、L1データキャッシュが2 * 256bit/8.59に到達し、ピーク2 * 256/8に非常に近く、使用率は約93%、FMAユニットは8/8.59のみを使用し、ピークは2 * 8であることがわかります。/8、使用率は47%です。
ですから、ピーター・コーデスが期待するように、私はL1Dのボトルネックに達したと思います。
================================================== ================
ボアーンに特に感謝します。私の質問の多くの文法エラーを修正してください。
================================================== ===============
Peterの回答から、「読み取りと書き込み」のレジスタのみが依存関係になり、「書き込み専用」のレジスタは依存関係にはならないことがわかりました。
そのため、ループで使用されるレジスタを減らして、展開を5つ減らします。すべて問題がなければ、同じボトルネックであるL1Dに遭遇するはずです。
.L2:
vmovaps ymm0, [rdi+rax]
vfmadd231ps ymm1, ymm0, [rsi+rax]
vmovaps ymm0, [rdi+rax+32]
vfmadd231ps ymm2, ymm0, [rsi+rax+32]
vmovaps ymm0, [rdi+rax+64]
vfmadd231ps ymm3, ymm0, [rsi+rax+64]
vmovaps ymm0, [rdi+rax+96]
vfmadd231ps ymm4, ymm0, [rsi+rax+96]
vmovaps ymm0, [rdi+rax+128]
vfmadd231ps ymm5, ymm0, [rsi+rax+128]
add rax, 160 ;n = n+32
jne .L2
結果:
Clock | Core cyc | Instruct | BrTaken | uop p0 | uop p1
------------------------------------------------------------------------
25332590 | 28547345 | 63700051 | 5100001 | 14951738 | 10549694
------------------------------------------------------------------------
4.97 | 5.60 | 12.49 | 1 | 2.93 | 2.07
uop p2 |uop p3 | uop p4 | uop p5 |uop p6 | uop p7
------------------------------------------------------------------------------
25900132 |25900132 | 50 | 683 | 5400909 | 9
-------------------------------------------------------------------------------
5.08 |5.08 | 0.00 | 0.00 |1.06 | 0.00
5/5.60 = 89.45%であることがわかります。これは、urollingよりも8だけ小さいので、何か問題がありますか?
================================================== ===============
結果を確認するために、6、7、および15までにループを展開しようとします。また、結果を再確認するために、もう一度5と8で展開します。
結果は次のとおりです。今回は、以前よりもはるかに良い結果が得られていることがわかります。
結果は安定していませんが、展開係数が大きく、結果は良好です。
| L1D bandwidth | CodeMiss | L1D Miss | L2 Miss
----------------------------------------------------------------------------
unroll5 | 91.86% ~ 91.94% | 3~33 | 272~888 | 17~223
--------------------------------------------------------------------------
unroll6 | 92.93% ~ 93.00% | 4~30 | 481~1432 | 26~213
--------------------------------------------------------------------------
unroll7 | 92.29% ~ 92.65% | 5~28 | 336~1736 | 14~257
--------------------------------------------------------------------------
unroll8 | 95.10% ~ 97.68% | 4~23 | 363~780 | 42~132
--------------------------------------------------------------------------
unroll15 | 97.95% ~ 98.16% | 5~28 | 651~1295 | 29~68
================================================== ===================
Webでgcc7.1を使用して関数をコンパイルしようとしています " https://gcc.godbolt.org "
コンパイルオプションは「-O3-march = haswell -mtune = intel」で、gcc4.8.3に似ています。
.L3:
vmovss xmm1, DWORD PTR [rdi+rax]
vfmadd231ss xmm0, xmm1, DWORD PTR [rsi+rax]
add rax, 4
cmp rdx, rax
jne .L3
ret
ループをもう一度見てください:_movss xmm1, src
_は、宛先が書き込み専用であるため、_xmm1
_の古い値に依存しません。 。各反復のmulss
は独立しています。アウトオブオーダー実行は、その命令レベルの並列性を悪用する可能性があり、実際に悪用するため、mulss
レイテンシーをボトルネックにすることは絶対にありません。
オプションの読み方:コンピュータアーキテクチャの用語で:レジスタの名前を変更すると、同じアーキテクチャレジスタを再利用する WARの依存関係防止データの危険性 が回避されます。 (レジスタの名前を変更する前の一部のパイプライン化+依存関係追跡スキームでは、すべての問題が解決されなかったため、コンピュータアーキテクチャの分野では、さまざまな種類のデータハザードから大きな問題が発生します。
Tomasuloのアルゴリズム を使用したレジスタの名前変更により、実際の真の依存関係(書き込み後の読み取り)を除くすべてが削除されるため、宛先がソースレジスタでもない命令は、古い値を含む依存関係チェーンと相互作用しません。そのレジスタの。 ( IntelCPUではpopcnt
のような誤った依存関係を除き、残りをクリアせずにレジスタの一部のみを書き込みます(_mov al, 5
_または_sqrtss xmm2, xmm1
_など)。関連: ほとんどのx64命令が32ビットレジスタの上部をゼロにする理由 )。
コードに戻る:
_.L13:
movss xmm1, DWORD PTR [rdi+rax*4]
mulss xmm1, DWORD PTR [rsi+rax*4]
add rax, 1
cmp cx, ax
addss xmm0, xmm1
jg .L13
_
ループで実行される依存関係(1つの反復から次の反復へ)はそれぞれ次のとおりです。
xmm0
_、_addss xmm0, xmm1
_によって読み書きされ、Haswellで3サイクルのレイテンシがあります。rax
、_add rax, 1
_によって読み取りおよび書き込み。 1cレイテンシーなので、クリティカルパスではありません。3c addss
レイテンシのループのボトルネックがあるため、実行時間/サイクルカウントを正しく測定したようです。
これは予想されることです。内積のシリアル依存関係は、ベクトル要素間の乗算ではなく、単一の合計への加算(別名、削減)です。
これは、さまざまな小さな非効率性にもかかわらず、このループの主なボトルネックです。
_short i
_は、余分なオペランドサイズのプレフィックスをとるばかげた_cmp cx, ax
_を生成しました。幸い、gccは実際に_add ax, 1
_を実行することを回避できました。これは、signed-overflowがCの未定義の動作であるためです。 したがって、オプティマイザーはそれが発生しないと想定できます 。 (更新: 整数プロモーションルールはshort
で異なるため、UBは含まれませんが、gccは合法的に最適化できます。かなり奇抜なものです。)
_-mtune=intel
_、またはそれ以上の_-march=haswell
_でコンパイルした場合、gccはcmp
とjg
をマクロ融合できる場所に並べて配置します。
cmp
およびadd
命令のテーブルに_*
_がある理由がわかりません。 (更新:私はあなたが [〜#〜] iaca [〜#〜] のような表記法を使用していると純粋に推測していましたが、明らかにそうではありませんでした)。それらのどちらも融合しません。発生している唯一の融合は、_mulss xmm1, [rsi+rax*4]
_のマイクロ融合です。
また、リードモディファイライトデスティネーションレジスタを備えた2オペランドのALU命令であるため、HaswellのROBでもマクロ融合されたままです。 (Sandybridgeは発行時にラミネートを解除します。) _vmulss xmm1, xmm1, [rsi+rax*4]
_はHaswellでもラミネートを解除することに注意してください 。
これは実際には重要ではありません。FP追加のレイテンシーを完全にボトルネックにしているだけで、uopスループットの制限よりもはるかに遅いからです。 _-ffast-math
_がなければ、コンパイラーができることは何もありません。 _-ffast-math
_を使用すると、clangは通常、複数のアキュムレータで展開され、自動ベクトル化されるため、ベクトルアキュムレータになります。したがって、L1Dキャッシュにヒットした場合は、Haswellのスループット制限である1ベクトルまたはスカラーFP 1クロックあたりの追加)を飽和させることができます。
HaswellでのFMAのレイテンシは5c、スループットは0.5cであるため、10個のFMAを飛行状態に保ち、p0/p1をFMAで飽和状態に保つことでFMAスループットを最大化するには10個のアキュムレータが必要です。 (SkylakeはFMAレイテンシーを4サイクルに短縮し、FMAユニットで乗算、加算、およびFMAを実行します。したがって、実際にはHaswellよりも加算レイテンシーが高くなります。)
(FMAごとに2つのロードが必要なため、ロードのボトルネックになっています。それ以外の場合は、vaddps
命令を乗数1.0のFMAに置き換えることで、実際にスループットを向上させることができます。非表示にするまでの待ち時間があるため、そもそもクリティカルパス上にない追加がある、より複雑なアルゴリズムに最適です。)
Re:ポートあたりのuops:
ポート5にはループごとに1.19uopsがあり、0.5をはるかに上回っています。これは、uopsディスパッチャがすべてのポートでuopsを同じにしようとしていることの問題です。
はい、そのようなものです。
Uopsはランダムに割り当てられていないか、実行可能なすべてのポートに均等に分散されています。 add
とcmp
uopsがp0156全体に均等に分散すると想定しましたが、そうではありません。
発行ステージでは、ポートを待機しているuopsの数に基づいて、uopsをポートに割り当てます。 addss
はp1でのみ実行できるため(これはループのボトルネックです)、通常、多くのp1 uopsが発行されますが、実行されません。そのため、port1にスケジュールされる他のuopsはほとんどありません。 (これにはmulss
が含まれます:ほとんどのmulss
uopsは最終的にポート0にスケジュールされます。)
Taken-branchsはポート6でのみ実行できます。ポート5には、このループ内にonlyのみ実行できるuopsがないため、多くの多くの人を引き付けることになります-ポートuops。
スケジューラー(リザベーションステーションから非融合ドメインuopsを選択する)は、クリティカルパスファーストを実行するほど賢くないため、これは割り当てアルゴリズムであり、リソース競合の待ち時間を短縮します(他のuopsは、addss
が実行された可能性があります)。また、特定のポートのスループットにボトルネックがある場合にも役立ちます。
私が理解しているように、すでに割り当てられているuopsのスケジューリングは、通常、最も古い準備ができています。この単純なアルゴリズムは、CPUを溶かすことなく、クロックサイクルごとに 60エントリRS から各ポートに入力できるuopを選択する必要があるため、驚くことではありません。 ILP を検出して悪用する異常な機械は、実際の作業を実行する実行ユニットに匹敵する、最新のCPUの重要な電力コストの1つです。
関連/詳細: x86 uopsは正確にどのようにスケジュールされていますか?
キャッシュミス/ブランチの予測ミス以外に、CPUバウンドループの3つの主なボトルネックは次のとおりです。
ループ本体またはコードの短いブロックは、融合ドメインuopカウント、実行可能な実行ユニットの非融合ドメインカウント、およびクリティカルパスのベストケーススケジューリングを想定したクリティカルパスの合計遅延の3つで大まかに特徴付けることができます。 。 (または、各入力A/B/Cから出力までのレイテンシー...)
いくつかの短いシーケンスを比較するために3つすべてを実行する例については、 ある位置以下のセットビットをカウントする効率的な方法は何ですか? に関する私の答えを参照してください。
短いループの場合、最新のCPUには、すべての並列処理を見つけるのに十分な実行中のループの反復を行うのに十分なアウトオブオーダー実行リソース(名前変更がレジスターを使い果たしないようにする物理レジスターファイルサイズ、ROBサイズ)があります。しかし、ループ内の依存関係チェーンが長くなると、最終的にはなくなります。 CPUが名前を変更するレジスタを使い果たしたときに何が起こるかについての詳細は、 リオーダーバッファ容量の測定 を参照してください。
x86 タグwikiの多くのパフォーマンスとリファレンスリンクも参照してください。
はい、Haswellのドット積は、乗算+加算ごとに2つの負荷がかかるため、FMAユニットのスループットの半分でL1Dスループットのボトルネックになります。
_B[i] = x * A[i] + y;
_またはsum(A[i]^2)
を実行している場合、FMAスループットが飽和する可能性があります。
vmovaps
ロードの宛先のような書き込み専用の場合でも、レジスタの再利用を回避しようとしているようです。そのため、8で展開した後にレジスタが不足しました。 。それは問題ありませんが、他の場合には問題になる可能性があります。
また、_ymm8-15
_を使用すると、2バイトではなく3バイトのVEXプレフィックスが必要になる場合、コードサイズがわずかに大きくなる可能性があります。おもしろい事実:_vpxor ymm7,ymm7,ymm8
_には3バイトのVEXが必要ですが、_vpxor ymm8,ymm8,ymm7
_には2バイトのVEXプレフィックスのみが必要です。可換演算の場合、ソースレジスタを高から低に並べ替えます。
負荷のボトルネックは、最良の場合のFMAスループットが最大値の半分であることを意味します。したがって、レイテンシーを隠すには、少なくとも5つのベクトルアキュムレーターが必要です。 8が適切であるため、依存関係チェーンには十分な余裕があり、予期しない遅延やp0/p1の競合による遅延の後に追いつくことができます。 7または6でも問題ありません。展開係数は、2の累乗である必要はありません。
正確に5で展開すると、依存関係チェーンのボトルネックにもなります。 FMAが正確なサイクルで実行されない場合は常に、入力の準備ができているということは、その依存関係チェーンでサイクルが失われたことを意味します。これは、ロードが遅い場合(たとえば、L1キャッシュでミスし、L2を待機する必要がある場合)、またはロードが順不同で完了し、別の依存関係チェーンからのFMAがこのFMAがスケジュールされたポートを盗む場合に発生する可能性があります。 (スケジューリングは発行時に行われるため、スケジューラーにあるuopsはport0FMAまたはport1FMAのいずれかであり、アイドル状態のポートを取得できるFMAではないことに注意してください)。
依存関係チェーンにいくらかの余裕を残しておくと、FMAがスループットやレイテンシーのボトルネックにならず、ロード結果を待つだけなので、アウトオブオーダー実行がFMAに「追いつく」可能性があります。 @Forwardは、(質問の更新で)展開すると、パフォーマンスがL1Dスループットの93%からこのループの89.5%に低下することを発見しました。
私の推測では、ここでは6(レイテンシーを隠すための最小値より1つ多い)で展開しても問題なく、8で展開するのとほぼ同じパフォーマンスが得られます(負荷でボトルネックになるだけでなく、FMAスループットの最大化に近づいた場合)スループット)、最小値より1つ多いだけでは不十分な場合があります。
更新:@Forwardの実験的テストは、私の推測が間違っていたことを示しています。 unroll5とunroll6の間に大きな違いはありません。また、unroll15はunroll8の2倍近く、クロックあたり2x256bロードの理論上の最大スループットに近いです。ループ内の独立した負荷のみ、または独立した負荷とレジスタのみのFMAを使用して測定すると、FMA依存関係チェーンとの相互作用によるものがどれだけあるかがわかります。最良の場合でも、測定エラーとタイマー割り込みによる中断が原因である場合でも、完全な100%のスループットは得られません。 (Linux perf
は、rootとして実行しない限り、ユーザースペースサイクルのみを測定しますが、時間には割り込みハンドラーで費やされた時間が含まれます。これが、非rootとして実行されたときにCPU周波数が3.87GHzとして報告される理由です。ただし、ルートとして実行し、_cycles:u
_ではなくcycles
を測定する場合は3.900GHz。)
フロントエンドのスループットにボトルネックはありませんが、mov
以外の命令のインデックス付きアドレッシングモードを回避することで、融合ドメインのuop数を減らすことができます。これ以外のものとコアを共有する場合は、少ない方が優れており、これをより多くハイパースレッディングフレンドリーにします。
簡単な方法は、ループ内で2つのポインターインクリメントを実行することです。複雑な方法は、一方の配列を他方の配列に対してインデックス付けする巧妙なトリックです。
_;; input pointers for x[] and y[] in rdi and rsi
;; size_t n in rdx
;;; zero ymm1..8, or load+vmulps into them
add rdx, rsi ; end_y
; lea rdx, [rdx+rsi-252] to break out of the unrolled loop before going off the end, with odd n
sub rdi, rsi ; index x[] relative to y[], saving one pointer increment
.unroll8:
vmovaps ymm0, [rdi+rsi] ; *px, actually py[xy_offset]
vfmadd231ps ymm1, ymm0, [rsi] ; *py
vmovaps ymm0, [rdi+rsi+32] ; write-only reuse of ymm0
vfmadd231ps ymm2, ymm0, [rsi+32]
vmovaps ymm0, [rdi+rsi+64]
vfmadd231ps ymm3, ymm0, [rsi+64]
vmovaps ymm0, [rdi+rsi+96]
vfmadd231ps ymm4, ymm0, [rsi+96]
add rsi, 256 ; pointer-increment here
; so the following instructions can still use disp8 in their addressing modes: [-128 .. +127] instead of disp32
; smaller code-size helps in the big picture, but not for a micro-benchmark
vmovaps ymm0, [rdi+rsi+128-256] ; be pedantic in the source about compensating for the pointer-increment
vfmadd231ps ymm5, ymm0, [rsi+128-256]
vmovaps ymm0, [rdi+rsi+160-256]
vfmadd231ps ymm6, ymm0, [rsi+160-256]
vmovaps ymm0, [rdi+rsi-64] ; or not
vfmadd231ps ymm7, ymm0, [rsi-64]
vmovaps ymm0, [rdi+rsi-32]
vfmadd231ps ymm8, ymm0, [rsi-32]
cmp rsi, rdx
jb .unroll8 ; } while(py < endy);
_
vfmaddps
のメモリオペランドとしてインデックス付けされていないアドレッシングモードを使用すると、問題でラミネートされていないのではなく、アウトオブオーダーコアでマイクロフュージョンされたままになります。 マイクロフュージョンおよびアドレッシングモード
したがって、私のループは、8つのベクトルに対して18の融合ドメインuopsです。インデックス付きアドレッシングモードのラミネーションが解除されているため、vmovaps + vfmaddpsのペアごとに2つではなく3つの融合ドメインuopsを使用します。もちろん、どちらもペアごとに2つの非融合ドメインロードuops(port2/3)があるため、それが依然としてボトルネックです。
融合ドメインのuopsが少ないと、アウトオブオーダー実行でより多くの反復が先に表示され、キャッシュミスをより適切に吸収できる可能性があります。ただし、キャッシュミスがなくても、実行ユニット(この場合はuopsをロード)でボトルネックになっている場合は、マイナーなことです。ただし、ハイパースレッディングでは、他のスレッドが停止しない限り、フロントエンド発行帯域幅の1サイクルおきにしか取得できません。ロードとp0/1の競合があまりない場合は、融合ドメインのuopsが少ないほど、コアを共有しながらこのループをより高速に実行できます。 (たとえば、他のハイパースレッドが多くのport5/port6を実行していて、uopsを格納している可能性がありますか?)
ラミネート解除はuop-cacheの後に行われるため、バージョンはuopキャッシュで余分なスペースを取りません。各uopのdisp32は問題なく、余分なスペースを取りません。ただし、コードサイズが大きくなると、uopキャッシュラインがいっぱいになる前に32Bの境界に達するため、uopキャッシュが効率的にパックされる可能性が低くなります。 (実際には、コードが小さいほど良いとは限りません。命令が小さいと、uopキャッシュ行がいっぱいになり、32B境界を越える前に別の行に1つのエントリが必要になる可能性があります。)この小さなループはループバックバッファー(LSD)から実行できるため、幸い、uop-cacheは要因ではありません。
次に、ループの後:効率的なクリーンアップは、展開係数または特にベクトル幅の倍数ではない可能性がある小さな配列の効率的なベクトル化の難しい部分です。
_ ...
jb
;; If `n` might not be a multiple of 4x 8 floats, put cleanup code here
;; to do the last few ymm or xmm vectors, then scalar or an unaligned last vector + mask.
; reduce down to a single vector, with a tree of dependencies
vaddps ymm1, ymm2, ymm1
vaddps ymm3, ymm4, ymm3
vaddps ymm5, ymm6, ymm5
vaddps ymm7, ymm8, ymm7
vaddps ymm0, ymm3, ymm1
vaddps ymm1, ymm7, ymm5
vaddps ymm0, ymm1, ymm0
; horizontal within that vector, low_half += high_half until we're down to 1
vextractf128 xmm1, ymm0, 1
vaddps xmm0, xmm0, xmm1
vmovhlps xmm1, xmm0, xmm0
vaddps xmm0, xmm0, xmm1
vmovshdup xmm1, xmm0
vaddss xmm0, xmm1
; this is faster than 2x vhaddps
vzeroupper ; important if returning to non-AVX-aware code after using ymm regs.
ret ; with the scalar result in xmm0
_
最後の水平方向の合計の詳細については、 x86で水平方向の浮動ベクトルの合計を行う最速の方法 を参照してください。私が使用した2つの128bシャッフルは、即時制御バイトを必要としないため、より明白なshufps
と比較して2バイトのコードサイズを節約します。 (そして、4バイトのコードサイズ対vpermilps
、そのオペコードは常に3バイトのVEXプレフィックスとイミディエートを必要とするため)。 AVX 3オペランドのものは非常にSSEと比較して優れています。特に、組み込み関数を使用してCで書き込む場合は、コールドレジスタをmovhlps
に簡単に選択することはできません。 。