web-dev-qa-db-ja.com

役に立たないMOV命令を導入すると、x86_64アセンブリのタイトループが高速化されるのはなぜですか?

背景:

組み込みアセンブリ言語を使用して Pascal コードを最適化する際に、不要なMOV命令に気付き、削除しました。

驚いたことに、不要な命令を削除すると、プログラムが遅くなりました

任意の無駄なMOV命令を追加すると、パフォーマンスがさらに向上することがわかりました

効果は不規則であり、実行順序に基づいて変化します:同じジャンク命令が1行ずつ上下に転置されたスローダウンを生成します

CPUはあらゆる種類の最適化と効率化を行うことを理解していますが、これは黒魔術のようです。

データ:

私のコードのバージョンは、2**20==1048576回実行するループの途中で条件付きで3つのジャンク操作をコンパイルします。 (周囲のプログラムは SHA-256 ハッシュを計算するだけです)。

私のかなり古いマシンでの結果(Intel(R)Core(TM)2 CPU 6400 @ 2.13 onGHz):

avg time (ms) with -dJUNKOPS: 1822.84 ms
avg time (ms) without:        1836.44 ms

プログラムはループで25回実行され、実行順序は毎回ランダムに変更されました。

抜粋:

{$asmmode intel}
procedure example_junkop_in_sha256;
  var s1, t2 : uint32;
  begin
    // Here are parts of the SHA-256 algorithm, in Pascal:
    // s0 {r10d} := ror(a, 2) xor ror(a, 13) xor ror(a, 22)
    // s1 {r11d} := ror(e, 6) xor ror(e, 11) xor ror(e, 25)
    // Here is how I translated them (side by side to show symmetry):
  asm
    MOV r8d, a                 ; MOV r9d, e
    ROR r8d, 2                 ; ROR r9d, 6
    MOV r10d, r8d              ; MOV r11d, r9d
    ROR r8d, 11    {13 total}  ; ROR r9d, 5     {11 total}
    XOR r10d, r8d              ; XOR r11d, r9d
    ROR r8d, 9     {22 total}  ; ROR r9d, 14    {25 total}
    XOR r10d, r8d              ; XOR r11d, r9d

    // Here is the extraneous operation that I removed, causing a speedup
    // s1 is the uint32 variable declared at the start of the Pascal code.
    //
    // I had cleaned up the code, so I no longer needed this variable, and 
    // could just leave the value sitting in the r11d register until I needed
    // it again later.
    //
    // Since copying to RAM seemed like a waste, I removed the instruction, 
    // only to discover that the code ran slower without it.
    {$IFDEF JUNKOPS}
    MOV s1,  r11d
    {$ENDIF}

    // The next part of the code just moves on to another part of SHA-256,
    // maj { r12d } := (a and b) xor (a and c) xor (b and c)
    mov r8d,  a
    mov r9d,  b
    mov r13d, r9d // Set aside a copy of b
    and r9d,  r8d

    mov r12d, c
    and r8d, r12d  { a and c }
    xor r9d, r8d

    and r12d, r13d { c and b }
    xor r12d, r9d

    // Copying the calculated value to the same s1 variable is another speedup.
    // As far as I can tell, it doesn't actually matter what register is copied,
    // but moving this line up or down makes a huge difference.
    {$IFDEF JUNKOPS}
    MOV s1,  r9d // after mov r12d, c
    {$ENDIF}

    // And here is where the two calculated values above are actually used:
    // T2 {r12d} := S0 {r10d} + Maj {r12d};
    ADD r12d, r10d
    MOV T2, r12d

  end
end;

自分で試してみてください:

コードはオンラインです GitHubで 自分で試してみたい場合。

私の質問:

  • レジスタの内容を RAM に無駄にコピーすると、パフォーマンスが向上するのはなぜですか?
  • 同じ役に立たない命令が、一部の回線で速度を上げ、他の回線で速度を落とすのはなぜですか?
  • この動作は、コンパイラーによって予想どおりに悪用される可能性がありますか?
215
tangentstorm

速度向上の最も可能性の高い原因は次のとおりです。

  • mOVを挿入すると、後続の命令が異なるメモリアドレスにシフトされます。
  • これらの移動された命令の1つは重要な条件分岐でした
  • その分岐は、分岐予測テーブルのエイリアシングのために誤って予測されていました
  • ブランチを移動するとエイリアスが削除され、ブランチを正しく予測できるようになりました

Core2は、条件ジャンプごとに個別の履歴レコードを保持しません。代わりに、すべての条件付きジャンプの共有履歴を保持します。 グローバル分岐予測 の欠点の1つは、異なる条件付きジャンプが相関していない場合、履歴が無関係な情報によって希釈されることです。

この小さな 分岐予測チュートリアル は、分岐予測バッファの仕組みを示しています。キャッシュバッファーは、分岐命令のアドレスの下位部分によってインデックスが付けられます。これは、2つの重要な非相関ブランチが同じ下位ビットを共有しない限り、うまく機能します。その場合、多くの予測ミスされた分岐を引き起こすエイリアシングになります(これにより、命令パイプラインが停止し、プログラムが遅くなります)。

分岐の予測ミスがパフォーマンスにどのように影響するかを理解したい場合は、この優れた答えをご覧ください。 https://stackoverflow.com/a/11227902/100164

コンパイラは通常、どのブランチがエイリアスになるか、およびそれらのエイリアスが重要かどうかを知るのに十分な情報を持っていません。ただし、その情報は CachegrindVTune などのツールを使用して実行時に決定できます。

140

読むことができます http://research.google.com/pubs/pub37077.html

TL; DR:nop命令をプログラムにランダムに挿入すると、パフォーマンスが5%以上簡単に向上します。また、コンパイラーはこれを簡単に活用できません。通常、分岐予測子とキャッシュの動作の組み合わせですが、それと同じようにできます。リザベーションステーションの機能停止(依存関係チェーンが壊れている場合や、明らかなリソースのオーバーサブスクリプションがまったくない場合でも)。

79
Jonas Maebe

現代のCPUでは、アセンブリ命令は、CPUに実行命令を提供するためのプログラマーにとって最後に見える層であるが、実際にはCPUによる実際の実行からのいくつかの層であると考えています。

最新のCPUは、 RISC / CISC CISC x86命令を、よりRISCの動作が多い内部命令に変換するハイブリッドです。さらに、アウトオブオーダー実行アナライザー、分岐プレディクター、Intelの「マイクロ操作融合」があり、命令を同時作業のより大きなバッチにグループ化しようとします( VLIW /- イタニウム チタン)。キャッシュ境界もあります。コードが大きい場合、コードをより速く実行できる可能性があります(キャッシュコントローラーがよりインテリジェントにスロットを挿入するか、長く保持する可能性があります)。

CISCには常にAssembly-to-microcode変換レイヤーがありましたが、ポイントは、最新のCPUでははるかに複雑なことです。最新の半導体製造工場の余分なトランジスタ領域をすべて使用すると、CPUはおそらくいくつかの最適化アプローチを並行して適用し、最後に最適な高速化を提供するものを選択できます。追加の命令は、CPUにバイアスをかけて、他よりも優れた最適化パスを使用する場合があります。

追加の命令の効果は、おそらくCPUモデル/世代/メーカーに依存し、予測可能ではないでしょう。この方法でアセンブリ言語を最適化するには、おそらくCPU固有の実行パスを使用して、多くのCPUアーキテクチャ世代に対して実行する必要があり、本当に重要なコードセクションにのみ望ましいでしょう。ただし、アセンブリを実行している場合は、おそらく既に知っています。

14
cowarldlydragon