webdevqa.jp.net

Intel Skylakeのストアループの予想外に貧弱で奇妙なバイモーダルパフォーマンス

2つのストアがある単純なストアループのパフォーマンスが予想外に低くなっています。1つは16バイトの順方向ストライドで、もう1つは常に同じ場所にあります。1、 このような:

volatile uint32_t value;

void weirdo_cpp(size_t iters, uint32_t* output) {

    uint32_t x = value;
    uint32_t          *rdx = output;
    volatile uint32_t *rsi = output;
    do {
        *rdx    = x;
        *rsi = x;

        rdx += 4;  // 16 byte stride
    } while (--iters > 0);
}

アセンブリでは、このループはおそらく3 次のようになります:

weirdo_cpp:

...

align 16
.top:
    mov    [rdx], eax  ; stride 16
    mov    [rsi], eax  ; never changes

    add    rdx, 16

    dec    rdi
    jne    .top

    ret

アクセスされたメモリ領域がL2にある場合、これは反復ごとに3サイクル未満で実行されると思います。 2番目のストアは同じ場所にヒットし続け、約1サイクル追加する必要があります。最初のストアは、L2から行を取り込み、行を削除することを意味します4回の反復ごとに1回。 L2のコストをどのように評価するかはわかりませんが、控えめに見積もっても、L1はサイクルごとに次のいずれかしか実行できません:(a)ストアをコミットするか(b)L2から回線を受信するか(c) L2への行を削除すると、ストライド16ストアストリームに対して1 + 0.25 + 0.25 = 1.5サイクルのようなものが得られます。

実際、1つのストアをコメントアウトすると、最初のストアのみで反復ごとに最大1.25サイクル、2番目のストアで反復ごとに最大1.01サイクルになるため、反復ごとに2.5サイクルは控えめな見積もりの​​ように見えます。

ただし、実際のパフォーマンスは非常に奇妙です。テストハーネスの一般的な実行は次のとおりです。

Estimated CPU speed:  2.60 GHz
output size     :   64 KiB
output alignment:   32
 3.90 cycles/iter,  1.50 ns/iter, cpu before: 0, cpu after: 0
 3.90 cycles/iter,  1.50 ns/iter, cpu before: 0, cpu after: 0
 3.90 cycles/iter,  1.50 ns/iter, cpu before: 0, cpu after: 0
 3.89 cycles/iter,  1.49 ns/iter, cpu before: 0, cpu after: 0
 3.90 cycles/iter,  1.50 ns/iter, cpu before: 0, cpu after: 0
 4.73 cycles/iter,  1.81 ns/iter, cpu before: 0, cpu after: 0
 7.33 cycles/iter,  2.81 ns/iter, cpu before: 0, cpu after: 0
 7.33 cycles/iter,  2.81 ns/iter, cpu before: 0, cpu after: 0
 7.34 cycles/iter,  2.81 ns/iter, cpu before: 0, cpu after: 0
 7.26 cycles/iter,  2.80 ns/iter, cpu before: 0, cpu after: 0
 7.28 cycles/iter,  2.80 ns/iter, cpu before: 0, cpu after: 0
 7.31 cycles/iter,  2.81 ns/iter, cpu before: 0, cpu after: 0
 7.29 cycles/iter,  2.81 ns/iter, cpu before: 0, cpu after: 0
 7.28 cycles/iter,  2.80 ns/iter, cpu before: 0, cpu after: 0
 7.29 cycles/iter,  2.80 ns/iter, cpu before: 0, cpu after: 0
 7.27 cycles/iter,  2.80 ns/iter, cpu before: 0, cpu after: 0
 7.30 cycles/iter,  2.81 ns/iter, cpu before: 0, cpu after: 0
 7.30 cycles/iter,  2.81 ns/iter, cpu before: 0, cpu after: 0
 7.28 cycles/iter,  2.80 ns/iter, cpu before: 0, cpu after: 0
 7.28 cycles/iter,  2.80 ns/iter, cpu before: 0, cpu after: 0

ここでは2つのことが奇妙です。

最初はバイモーダルタイミングです。高速モード低速モードがあります。 低速モードで開始し、反復ごとに約7.3サイクルかかり、ある時点で反復ごとに約3.9サイクルに移行します。この動作は一貫していて再現性があり、2つのタイミングは常に2つの値の周りにクラスター化されて非常に一貫しています。遷移は、低速モードから高速モードへの両方向に表示され、その逆もあります(1回の実行で複数の遷移が表示される場合もあります)。

他の奇妙なことは本当に悪いパフォーマンスです。 高速モードの場合でも、約3.9サイクルでは、パフォーマンスは1.0 + 1.3 = 2.3サイクルの最悪のキャストよりもはるかに悪くなります。両方のストアがループ内にある場合、完全にゼロの作業が重複する可能性があります)。 低速モードでは、最初の原則に基づいて期待するものと比較してパフォーマンスがひどいです。2つのストアを実行するのに7.3サイクルかかります。これを、L2ストアの帯域幅の条件で表すと、おおよそL2ストアごとに29サイクル(4回の反復ごとに1つの完全なキャッシュラインのみを格納するため)。

Skylakeは 記録 L1とL2の間に64B /サイクルのスループットがあります。これはwayここで観察されたスループットよりも高いです(遅い)で約2バイト/サイクルモード)。

スループットの低下とバイモーダルパフォーマンスの理由は何ですか?それを回避できますか?

これが他のアーキテクチャや他のSkylakeボックスでも再現されるかどうかも知りたいです。コメントにローカルの結果を自由に含めてください。

githubのテストコードとハーネス を見つけることができます。 LinuxまたはUnixライクなプラットフォーム用のMakefileがありますが、Windows上でも比較的簡単に構築できるはずです。 asmバリアントを実行する場合は、アセンブリにnasmまたはyasmが必要です。4 -それがない場合は、C++バージョンを試すことができます。

排除された可能性

これが私が考慮し、大部分を排除したいくつかの可能性です。多くの可能性は、パフォーマンスの遷移がランダムに表示されるという単純な事実によって排除されますベンチマークループの途中で、多くのものが単に変更されていない場合(たとえば、出力に関連している場合)配列の配置。同じバッファーが常に使用されているため、実行の途中で変更できませんでした)。以下では、これをデフォルトの除去と呼びます(デフォルトの除去である場合でも、別の議論が行われることがよくあります)。

  • アラインメントファクター:出力配列は16バイトアラインメントされており、変更せずに最大2MBのアラインメントを試しました。 デフォルトの削除によっても削除されます。
  • マシン上の他のプロセスとの競合:影響は、アイドル状態のマシンでも、負荷の高いマシンでもほぼ同じように観察されます(たとえば、stress -vm 4を使用)。ベンチマーク自体はL2に収まるため、とにかく完全にコアローカルである必要があります。perfは、反復ごとにL2ミスがほとんどないことを確認します(300〜400回の反復ごとに約1ミス、おそらくprintfコード)。
  • TurboBoost:TurboBoostは完全に無効になっており、3つの異なるMHz測定値で確認されています。
  • 省電力機能:パフォーマンスガバナーはperformanceモードのintel_pstateです。テスト中に周波数変動は観察されません(CPUは基本的に2.59 GHzでロックされたままです)。
  • TLB効果:この効果は、出力バッファーが2MBの巨大なページにある場合でも存在します。いずれにせよ、64個の4kTLBエントリは128K出力バッファをカバーします。 perfは、特に奇妙なTLBの動作を報告しません。
  • 4kエイリアシング:このベンチマークの古い、より複雑なバージョンでは4kエイリアシングが見られましたが、ベンチマークに負荷なしがあるため、これは削除されました(以前のストアを誤ってエイリアシングする可能性のある負荷です)。 デフォルトの削除によっても削除されます。
  • L2の結合性の競合:デフォルトの除去と、出力バッファーが物理メモリに線形に配置されていることを確認できる2MBページでもこれが解消されないという事実によって除去されます。
  • ハイパースレッディング効果:HTは無効になっています。
  • プリフェッチ:すべてのデータがL1またはL2に存在するため、ここでは2つのプリフェッチャー(「DCU」、別名L1 <-> L2プリフェッチャー)のみを使用できますが、パフォーマンスはすべてのプリフェッチャーを有効またはすべて無効にしても同じです。
  • 割り込み:割り込みカウントと低速モードの間に相関関係はありません。割り込みの総数は限られており、ほとんどがクロックティックです。

toplev.py

私は toplev.py を使用しました。これはIntelの Top Down 分析方法を実装しており、当然のことながら、ベンチマークをストアバウンドとして識別します。

BE             Backend_Bound:                                                      82.11 % Slots      [  4.83%]
BE/Mem         Backend_Bound.Memory_Bound:                                         59.64 % Slots      [  4.83%]
BE/Core        Backend_Bound.Core_Bound:                                           22.47 % Slots      [  4.83%]
BE/Mem         Backend_Bound.Memory_Bound.L1_Bound:                                 0.03 % Stalls     [  4.92%]
    This metric estimates how often the CPU was stalled without
    loads missing the L1 data cache...
    Sampling events:  mem_load_retired.l1_hit:pp mem_load_retired.fb_hit:pp
BE/Mem         Backend_Bound.Memory_Bound.Store_Bound:                             74.91 % Stalls     [  4.96%] <==
    This metric estimates how often CPU was stalled  due to
    store memory accesses...
    Sampling events:  mem_inst_retired.all_stores:pp
BE/Core        Backend_Bound.Core_Bound.Ports_Utilization:                         28.20 % Clocks     [  4.93%]
BE/Core        Backend_Bound.Core_Bound.Ports_Utilization.1_Port_Utilized:         26.28 % CoreClocks [  4.83%]
    This metric represents Core cycles fraction where the CPU
    executed total of 1 uop per cycle on all execution ports...
               MUX:                                                                 4.65 %           
    PerfMon Event Multiplexing accuracy indicator

これはあまり光を当てません。私たちは、それが物事を台無しにしている店でなければならないことをすでに知っていましたが、なぜですか? Intelの説明 条件の多くは言いません。

ここに L1-L2の相互作用に関連するいくつかの問題の合理的な要約。


2019年2月の更新:パフォーマンスの「バイモーダル」部分を再現できなくなりました:私にとって、同じi7-6700HQボックスでのパフォーマンスは今常に同じ場合に非常に遅いバイモーダルパフォーマンスが適用されます。つまり、次のように1行あたり約16〜20サイクルの結果になります。

Everything slow now

この変更は、2018年8月のSkylakeマイクロコードアップデート、リビジョン0xC6で導入されたようです。以前のマイクロコード0xC2は、質問で説明されている元の動作を示しています。


1 これは私の元のループの大幅に簡略化されたMCVEであり、少なくとも3倍のサイズで、多くの追加作業を行いましたが、この単純なバージョンとまったく同じパフォーマンスを示し、同じ不思議な問題でボトルネックになりました。

3 特に、アセンブリを手動で記述した場合、またはgcc -O1(バージョン5.4.1)、およびおそらく最も妥当なコンパイラ(volatileは、ほとんど死んでいる2番目のストアがループの外に沈むのを防ぐために使用されます)。

4 アセンブリは非常に簡単なので、少し編集するだけでこれをMASM構文に変換できることは間違いありません。プルリクエストを受け付けました。

25
BeeOnRope

私がこれまでに見つけたもの。残念ながら、パフォーマンスの低下についての説明は実際には提供されておらず、バイモーダルディストリビューションについてもまったく説明されていませんが、パフォーマンスとそれを軽減するための注意事項が表示される場合の一連のルールです。

  • L2へのストアスループットは、3サイクルあたり最大1つの64バイトキャッシュラインのようです。、ストアのスループットにサイクルあたり最大21バイトの上限を設定します。別の言い方をすれば、L1でミスし、L2でヒットする一連のストアには、タッチされたキャッシュラインごとに少なくとも 3サイクルかかります。
  • そのベースラインを超えると、L2でヒットしたストアがインターリーブで、別のキャッシュライン(これらのストアがL1またはL2でヒットしたかどうかに関係なく)の場合、重大なペナルティが発生します。
  • nearby(ただし、同じキャッシュラインにはない)のストアの場合、ペナルティは明らかにいくらか大きくなります。
  • バイモーダルパフォーマンスは、少なくとも表面的には上記の効果に関連しています。これは、インターリーブしない場合は発生していないように見えるためですが、これ以上の説明はありません。
  • プリフェッチまたはダミーロードによって、ストアの前にキャッシュラインがすでにL1にあることを確認すると、パフォーマンスの低下がなくなり、パフォーマンスがバイモーダルではなくなります。

詳細と写真

64バイトのストライド

元の質問は任意に16のストライドを使用していましたが、おそらく最も単純なケースである64のストライド、つまり1つのフルキャッシュラインから始めましょう。さまざまな効果がどのストライドでも表示されることが判明しましたが、64はすべてのストライドでL2キャッシュミスを保証するため、いくつかの変数を削除します。

とりあえず2番目のストアも削除しましょう。64Kのメモリを超える単一の64バイトのストライドストアをテストしているだけです。

top:
mov    BYTE PTR [rdx],al
add    rdx,0x40
sub    rdi,0x1
jne    top

上記と同じハーネスでこれを実行すると、約3.05サイクル/ストアになります2、私が見慣れているものと比べるとかなりの違いがありますが(-そこには3.0もあります)。

ですから、純粋にL2までの持続的な店舗では、おそらくこれよりもうまくいくことはないでしょう。1。 Skylakeは明らかにL1とL2の間で64バイトのスループットを持っていますが、ストアのストリームの場合、その帯域幅はL1からの両方のエビクションで共有され、新しいラインをL1にロードする必要があります。 (a)L1からL2へのダーティビクティムラインの削除、(b)L2からの新しいラインでのL1の更新、および(c)ストアのL1へのコミットにそれぞれ1サイクルかかる場合、3サイクルは妥当と思われます。

ループ内の同じキャッシュライン(重要ではないことが判明しましたが、次のバイト)に2回目の書き込みを追加するとどうなりますか?このような:

top:
mov    BYTE PTR [rdx],al
mov    BYTE PTR [rdx+0x1],al
add    rdx,0x40
sub    rdi,0x1
jne    top

上記のループのテストハーネスを1000回実行するタイミングのヒストグラムは次のとおりです。

  count   cycles/itr
      1   3.0
     51   3.1
      5   3.2
      5   3.3
     12   3.4
    733   3.5
    139   3.6
     22   3.7
      2   3.8
     11   4.0
     16   4.1
      1   4.3
      2   4.4

したがって、ほとんどの時間は約3.5サイクルでクラスター化されます。つまり、この追加ストアはタイミングに0.5サイクルしか追加しませんでした。同じ行にある場合、ストアバッファが2つのストアをL1に排出できるようなものである可能性がありますが、これは約半分の時間でしか発生しません。

ストアバッファに1, 1, 2, 2, 3, 3のような一連のストアが含まれていることを考慮してください。ここで、1はキャッシュラインを示します。位置の半分には同じキャッシュラインからの2つの連続する値があり、残りの半分にはありません。ストアバッファがストアのドレインを待機しており、L1がL2への回線の立ち退きと受け入れに忙しいため、L1は「任意の」ポイントでストアに使用できるようになります。位置が1, 1の場合店舗は1サイクルで排出されるかもしれませんが、1, 2の場合は2サイクルかかります。

3.5ではなく3.1付近に結果の約6%の別のピークがあることに注意してください。それは、私たちが常に幸運な結果を得る定常状態である可能性があります。 〜4.0-4.1で約3%の別のピークがあります-「常に不運な」配置。

1番目と2番目のストア間のさまざまなオフセットを調べて、この理論をテストしてみましょう。

top:
mov    BYTE PTR [rdx + FIRST],al
mov    BYTE PTR [rdx + SECOND],al
add    rdx,0x40
sub    rdi,0x1
jne    top

FIRSTSECONDのすべての値を0から256まで8ステップで試します。結果は、縦軸のFIRST値とSECONDを変化させたものです。水平方向:

cycles/iter for varying store offsets

特定のパターンが見られます。白い値は「高速」です(オフセット1について上記で説明した3.0〜4.1の値付近)。黄色の値は高く、最大8サイクル、赤は最大10です。紫色の外れ値が最も高く、通常、OPで説明されている「低速モード」が開始される場合です(通常は18.0サイクル/イターでクロッキング)。次のことに気づきました。

  • 白血球のパターンから、2番目のストアが最初のストアと比較して同じキャッシュラインまたは次のにある限り、約3.5サイクルの高速結果が得られることがわかります。これは、同じキャッシュラインへのストアがより効率的に処理されるという上記の考え方と一致しています。次のキャッシュラインに2番​​目のストアがあることが機能する理由は、最初の最初のアクセスを除いて、パターンが同じになるためです:0, 0, 1, 1, 2, 2, ... vs 0, 1, 1, 2, 2, ...-2番目の場合は、各キャッシュラインに最初にアクセスする2番目のストア。ただし、ストアバッファは関係ありません。別のキャッシュラインに入るとすぐに、0, 2, 1, 3, 2, ...のようなパターンが表示されますが、これはどうやらダメですか?

  • 紫色の「外れ値」が白い領域に表示されることはないため、すでに遅いシナリオに限定されているようです(ここでの速度が遅いほど、約2.5倍遅くなります:約8〜18サイクル)。

少しズームアウトして、さらに大きなオフセットを見ることができます。

offsets up to 2048

同じ基本パターンですが、2番目のストアが最初のストアから離れる(前または後ろ)と、約1700バイトのオフセットで再び悪化するまで、パフォーマンスが向上する(緑色の領域)ことがわかります。改善された領域でも、同じラインのパフォーマンスである3.5よりも、せいぜい5.8サイクル/反復しか達成できません。

追加した場合any先に実行される一種のロードまたはプリフェッチ命令3 店舗のうち、全体的な低速パフォーマンスと「低速モード」の外れ値の両方が消えます。

all good

これを元のストライドバイ16の問題に移植して戻すことができます-コアループ内のあらゆるタイプのプリフェッチまたはロードで、距離にほとんど影響されません(実際にはbehindであっても)、問題を修正し、 2.3サイクル/反復が得られ、2.0の可能な限り最良の理想に近く、別々のループを持つ2つのストアの合計に等しくなります。

したがって、基本的なルールは、対応するロードのないL2へのストアは、ソフトウェアがそれらをプリフェッチする場合よりもはるかに遅いということです-ストアストリーム全体がsingleシーケンシャルパターンでキャッシュラインにアクセスしない限り。これは、このような線形パターンがSWプリフェッチの恩恵を受けることは決してないという考えとは反対です。

私は実際に具体的な説明を持っていませんが、それはこれらの要因を含む可能性があります:

  • ストアバッファに他のストアがあると、L2に送信されるリクエストの同時実行性が低下する可能性があります。 L1でミスするストアがいつストアバッファーを割り当てるかは正確にはわかりませんが、ストアがリタイアする直前に発生し、場所を移動するためにストアバッファーに一定量の「ルックヘッド」が存在する可能性があります。 L1であるため、L1で見逃すことのない追加のストアがあると、ルックアヘッドは見逃すリクエストの数を確認できないため、同時実行性が損なわれます。
  • おそらく、読み取りポートと書き込みポート、キャッシュ間帯域幅などのL1リソースとL2リソースの競合があり、このパターンのストアではさらに悪化します。たとえば、異なるラインへのストアがインターリーブする場合、ストアキューからすぐに排出できない可能性があります(シナリオによっては、サイクルごとに複数のストアが排出される可能性があると思われる上記を参照してください)。

これらのコメント IntelフォーラムのDr.McCalpinによるものも非常に興味深いものです。


ほとんどの場合、L2ストリーマーを無効にした場合にのみ達成可能です。それ以外の場合、L2での追加の競合により、3.5サイクルあたり約1行に速度が低下します。

1 これを、ロードあたりほぼ正確に1.5サイクルを取得するストアと比較してください。暗黙の帯域幅は、サイクルあたり約43バイトです。これは完全に理にかなっています。L1<-> L2帯域幅は64バイトですが、L1がどちらか L2からの回線を受け入れると仮定するとまたはコアからのロード要求を処理しますサイクルごとに(両方を並列にではなく)、異なるL2ラインへの2つのロードに対して3サイクルがあります。L2からのラインを受け入れるための2サイクルと、2つのロード命令を満たすための1サイクルです。

2 プリフェッチありoff。結局のところ、L2プリフェッチャーはストリーミングアクセスを検出すると、L2キャッシュへのアクセスを競合します。常に候補行を見つけて、L3に移動しない場合でも、コードの速度が低下し、変動性が高まります。結論は通常、プリフェッチをオンにした場合に当てはまりますが、すべてが少し遅くなります(これは、プリフェッチをオンにした場合の 結果の大きな塊 です-ロードごとに約3.3サイクルが表示されますが、変動が大きくなります)。

3 本当に先に進む必要はありません。数行後ろにプリフェッチすることもできます。プリフェッチ/ロードは、ボトルネックになっているストアよりもすぐに実行されるため、とにかく先に進みます。このように、プリフェッチは一種の自己修復であり、入力したほとんどすべての値で機能するようです。

11
BeeOnRope

Sandy Bridgeには、「L1データハードウェアプリフェッチャー」があります。これが意味するのは、最初にストアを実行するときに、CPUがL2からL1にデータをフェッチする必要があるということです。しかし、これが数回発生した後、ハードウェアプリフェッチャーはNiceシーケンシャルパターンに気づき、L2からL1へのデータのプリフェッチを開始します。そのため、データはL1にあるか、コードがお店。

0
Brendan