web-dev-qa-db-ja.com

Agnerの命令テーブルとは異なり、mulssがHaswellで3サイクルしかかからないのはなぜですか? (展開FP複数のアキュムレータを使用したループ)

私は命令最適化の初心者です。

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
34
Forward

ループをもう一度見てください:_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はcmpjgをマクロ融合できる場所に並べて配置します。

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はランダムに割り当てられていないか、実行可能なすべてのポートに均等に分散されていますaddcmp 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つの主なボトルネックは次のとおりです。

  • 依存関係チェーン(この場合のように)
  • フロントエンドスループット(Haswellでクロックごとに発行される最大4つの融合ドメインuops)
  • 展開されたループのように、多くのuopがp0/p1またはp2/p3を必要とする場合など、実行ポートのボトルネック。特定のポートのunfused-domainuopsをカウントします。一般に、他のポートで実行できるuopsは、ビジー状態のポートを頻繁に盗むことはなく、最良の配布を想定できますが、実際に発生することもあります。

ループ本体またはコードの短いブロックは、融合ドメインuopカウント、実行可能な実行ユニットの非融合ドメインカウント、およびクリティカルパスのベストケーススケジューリングを想定したクリティカルパスの合計遅延の3つで大まかに特徴付けることができます。 。 (または、各入力A/B/Cから出力までのレイテンシー...)

いくつかの短いシーケンスを比較するために3つすべてを実行する例については、 ある位置以下のセットビットをカウントする効率的な方法は何ですか? に関する私の答えを参照してください。

短いループの場合、最新のCPUには、すべての並列処理を見つけるのに十分な実行中のループの反復を行うのに十分なアウトオブオーダー実行リソース(名前変更がレジスターを使い果たしないようにする物理レジスターファイルサイズ、ROBサイズ)があります。しかし、ループ内の依存関係チェーンが長くなると、最終的にはなくなります。 CPUが名前を変更するレジスタを使い果たしたときに何が起こるかについての詳細は、 リオーダーバッファ容量の測定 を参照してください。

x86 タグwikiの多くのパフォーマンスとリファレンスリンクも参照してください。


FMAループの調整:

はい、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に簡単に選択することはできません。 。

31
Peter Cordes