背景:
組み込みアセンブリ言語を使用して 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で 自分で試してみたい場合。
私の質問:
速度向上の最も可能性の高い原因は次のとおりです。
Core2は、条件ジャンプごとに個別の履歴レコードを保持しません。代わりに、すべての条件付きジャンプの共有履歴を保持します。 グローバル分岐予測 の欠点の1つは、異なる条件付きジャンプが相関していない場合、履歴が無関係な情報によって希釈されることです。
この小さな 分岐予測チュートリアル は、分岐予測バッファの仕組みを示しています。キャッシュバッファーは、分岐命令のアドレスの下位部分によってインデックスが付けられます。これは、2つの重要な非相関ブランチが同じ下位ビットを共有しない限り、うまく機能します。その場合、多くの予測ミスされた分岐を引き起こすエイリアシングになります(これにより、命令パイプラインが停止し、プログラムが遅くなります)。
分岐の予測ミスがパフォーマンスにどのように影響するかを理解したい場合は、この優れた答えをご覧ください。 https://stackoverflow.com/a/11227902/100164
コンパイラは通常、どのブランチがエイリアスになるか、およびそれらのエイリアスが重要かどうかを知るのに十分な情報を持っていません。ただし、その情報は Cachegrind や VTune などのツールを使用して実行時に決定できます。
読むことができます http://research.google.com/pubs/pub37077.html
TL; DR:nop命令をプログラムにランダムに挿入すると、パフォーマンスが5%以上簡単に向上します。また、コンパイラーはこれを簡単に活用できません。通常、分岐予測子とキャッシュの動作の組み合わせですが、それと同じようにできます。リザベーションステーションの機能停止(依存関係チェーンが壊れている場合や、明らかなリソースのオーバーサブスクリプションがまったくない場合でも)。
現代のCPUでは、アセンブリ命令は、CPUに実行命令を提供するためのプログラマーにとって最後に見える層であるが、実際にはCPUによる実際の実行からのいくつかの層であると考えています。
最新のCPUは、 RISC / CISC CISC x86命令を、よりRISCの動作が多い内部命令に変換するハイブリッドです。さらに、アウトオブオーダー実行アナライザー、分岐プレディクター、Intelの「マイクロ操作融合」があり、命令を同時作業のより大きなバッチにグループ化しようとします( VLIW /- イタニウム チタン)。キャッシュ境界もあります。コードが大きい場合、コードをより速く実行できる可能性があります(キャッシュコントローラーがよりインテリジェントにスロットを挿入するか、長く保持する可能性があります)。
CISCには常にAssembly-to-microcode変換レイヤーがありましたが、ポイントは、最新のCPUでははるかに複雑なことです。最新の半導体製造工場の余分なトランジスタ領域をすべて使用すると、CPUはおそらくいくつかの最適化アプローチを並行して適用し、最後に最適な高速化を提供するものを選択できます。追加の命令は、CPUにバイアスをかけて、他よりも優れた最適化パスを使用する場合があります。
追加の命令の効果は、おそらくCPUモデル/世代/メーカーに依存し、予測可能ではないでしょう。この方法でアセンブリ言語を最適化するには、おそらくCPU固有の実行パスを使用して、多くのCPUアーキテクチャ世代に対して実行する必要があり、本当に重要なコードセクションにのみ望ましいでしょう。ただし、アセンブリを実行している場合は、おそらく既に知っています。