HOW TO OPTIMIZE FOR THE PENTIUM PROCESSOR (In Japanese)

Original (in English): http://announce.com/agner/assem/assem.html
Copyright (c) 1996, 1997 by Agner Fog. Last modified 1997-08-19.

このページは、Agner Fogさんによる同名のマニュアルの、藤波順久による日本語訳です。原文(英語)の著作権はAgner Fogさんにあります。また、日本語訳中の「私」とは、Agner Fogさんのことです。原文は
http://announce.com/agner/assem/assem.html
を参照してください。

このページは、現在更新中であり、原文の古い版(1997-03-16)に基づいている章がいくつかあります。

目次

  1. はじめに
  2. 文献
  3. デバッグと確認
  4. メモリモデル
  5. アラインメント
  6. キャッシュ
  7. 番地生成インターロック(PPlain and PMMX)
  8. 整数命令のペアリング(PPlain and PMMX)
  9. 初回の実行と、繰り返し実行
  10. 不完全なペアリング(PPlain and PMMX)
  11. 複雑な命令を単純な命令に分割(PPlain and PMMX)
  12. ジャンプとブランチ
  13. プリフィクス
  14. コードサイズの縮小
  15. 浮動小数点コードのスケジューリング(PPlain and PMMX)
  16. ループの最適化(PPlain and PMMX)
  17. 特殊な命令について
  18. 整数命令を使った浮動小数点演算
  19. 浮動小数点命令を使った整数演算
  20. 整数命令のリスト(PPlain and PMMX)
  21. 浮動小数点命令のリスト(PPlain and PMMX)
  22. MMX命令(PMMX)
  23. コードの速度のテスト
  24. (準備中)Pentium ProとPentium IIプロセッサ
  25. (準備中)いろいろなマイクロプロセッサの比較

1. はじめに

このマニュアルは、最適化されたアセンブリ言語のコードの書き方について、詳述する。特に、Pentium(R)ファミリ・マイクロプロセッサに焦点をあてる。

このマニュアルは他の情報源に比べて理解しやすく正確で、他に見られない細かい事項をたくさん含んでいる。この情報を使えば、あなたは多くの場合に、あるコード片が正確に何クロックサイクルかかるのか計算することができるようになる。

このマニュアルでは次のようなバージョンのPentiumプロセッサについて議論する。

略称            名前
------------------------------------------------
PPlain          plain old Pentium (without MMX)
PMMX            Pentium with MMX
PPro            Pentium Pro
PII             Pentium II
------------------------------------------------

このマニュアルの情報は、私自身の調査と試験に基づいており、さまざまな人たちから受け取った情報で補足されている。このマニュアルのために追加情報を私に送ってくれた人々に感謝したい。これまでの私の調査はPPlainとPMMXしかカバーしていない。PProとPIIについては簡単に記述されているだけである。

アセンブリ言語でのプログラミングは、高級言語よりはるかに難しい。バグを生成するのは容易であり、その発見はたいへん困難である。ここで警告である! 読者は既にアセンブリ言語の経験があると仮定する。もしそうでなければ、複雑な最適化を始める前に、どうかアセンブリ言語に関する本を何か読んで、プログラミングの経験を得てほしい。

Pentiumチップのハードウェア設計は、一般的な最適化方法を使ったというよりはむしろ、いくつかのよく使われる命令やその組合せに特に最適化された、多くの特徴を持っている。その結果、ソフトウェアをこの設計向きに最適化するのはかなり複雑で、多くの例外があるが、相当な性能向上が可能かもしれない。

コードをアセンブリ言語に変換する前に、使っているアルゴリズムが最適であることを確認してほしい。コード片をアセンブラコードに直すよりアルゴリズムを改良したほうがずっとよい結果になることがしばしばある。

次に、プログラムの決定的に重要な部分を同定しなければならない。しばしば、99%以上のCPU時間がプログラムの最も内側のループで消費されている。この場合、そのループだけを最適化し、それ以外はすべて高級言語のままにしておくべきである。アセンブラプログラマの中には、プログラムの誤った部分を最適化するのにエネルギーを浪費し、努力の主な効果が、プログラムのデバッグや保守を難しくしただけという人もいる!

もしプログラムの決定的に重要な部分がどこか明らかでなければ、プロファイラを使ってみつけるとよい。もしボトルネックがディスクアクセスであるとわかったら、アセンブリプログラミングに行くのではなくて、ディスクアクセスをシーケンシャルに行うようにプログラムを変更して、ディスクのキャッシングを改良するとよい。もしボトルネックがグラフィクスの出力なら、グラフィクスの手続きを呼ぶ回数を減らす方法を探すとよい。

高級言語のコンパイラのいくつかは比較的よいPentium向けの最適化を提供しており、手でさらに最適化することは、最も決定的なコード片に対して(または娯楽のため)以外はその努力に見合わないかもしれない。

どうか私にプログラミングの質問を送らないでほしい。私はあなたの宿題をするつもりはない。

ナノ秒狩りの幸運を祈る!

PS. 私は故意に「optimization」を間違って綴っている。なぜなら、「optimation」のほうが発音に最適だからである。


2. 文献

たくさんの有用な文献が、インテルのWWWサイトから無料でダウンロード可能であり、また印刷物やCD-ROMとして得ることができる。

文献のURLは頻繁に変わるので、ここで紹介しない。
http://www.intel.com/sites/developer/search.htmの検索機能を使うか、
http://announce.com/agner/assemからリンクをたどることで、必要な文書を見つけることができる。

いくつかの文書は.PDF形式である。.PDFファイルを見たり印刷したりするソフトウェアを持っていなければ、www.adobe.comからAcrobatファイルリーダをダウンロードすればよい。

特定のアプリケーションを最適化するためにMMX命令を使うことは、アプリケーションノートのいくつかで述べられている。MMX命令セットはいろいろなマニュアルやチュートリアルで述べられている。

インテル以外にも有用な情報源がたくさんある。これらはニュースグループcomp.lang.asm.x86のFAQにリストされている。シェアウェアのエディタASMEDITには、すべての命令コードなどをカバーするオンラインヘルプがある。ASMEDITは
http://www.inf.tu-dresden.de/~ok3/asmedit.htmlから得られる。

インタネットのリソースについては
http://announce.com/agner/assem/からリンクをたどってほしい。


3. デバッグと確認

アセンブリコードをデバッグするのは、あなたがすでに気づいているかもしれないように、たいそう困難でいらいらする。私は次のようにすることを勧めたい。まず最適化したいコード片を高級言語のサブルーチンとして書くことから始め、次に、そのサブルーチンをすっかりテストするテストプログラムを書く。テストプログラムはすべての分岐や特別な場合を必ず通るようにする。

高級言語で書いたサブルーチンがテストプログラムと共に動くようになったら、そのコードをアセンブリ言語に直す準備ができたことになる。インラインアセンブラを使ってもよいし、サブルーチン全体をアセンブリ言語で書いて、リンクしてもよい。後者を選んだ場合、高級言語のコードを直接アセンブリ言語に変換できるコンパイラを使うことを勧める。これであなたは確実に、関数呼び出し法を正しくできる。たいていのC++コンパイラはこれができる。(最近のバージョンのマイクロソフトvisual C++はできない。バージョン9以前のCL.EXEを使うとよい)。

関数の呼び出しの方法と名前の変換はたいそう複雑である。多数の異なる呼び出し規約があり、コンパイラのブランドが異なればこの点で互換性がない。アセンブリ言語のサブルーチンをC++から呼び出そうとしているなら、整合性と互換性の点から最もよい方法は、関数を extern "C" と _cdecl で宣言することである。アセンブリ言語のコードは、アンダースコア(_)が先頭についた関数名を持ち、外部名の大文字小文字を区別する(オプション -mx)ようにアセンブルされるはずである。引数はスタックに逆順に積まれ、最初の引数が最下位の番地に来る。引数は呼び出された関数ではなく呼び出し側がスタックから取り除く。戻り値はレジスタeaxまたはst(0)にはいる。関数は、eax,ecx,edx(16ビットモードではax,bx,cx,dx,es)を除いて、使うレジスタをすべて保存し、戻る前に復元する。もし、名前の変換や別の呼び出し規約が必要なら、アセンブリ言語のコードを正しくするために、高級言語をアセンブリ言語に変換できるコンパイラを使わなければならない。

これで最適化を始めることができる。変更をするたびにコードをテストプログラム上で走らせて、正しく動くか見るべきである。

すべてのバージョンに番号をつけて保存せよ。そうすれば、テストプログラムではつかまらなかった(間違った番地に書き込んでしまうような)エラーを見つけた場合に戻ってテストし直せる。

プログラムの最も決定的な部分の速度を23章で述べる方法でテストせよ。コードが期待に比べてはっきり遅ければ、最もありそうな理由は、キャッシュミス(6章)、オペランドのミスアラインメント(5章)、最初の実行のペナルティー(9章)、分岐予測ミス(12章)である。


4. メモリモデル

Pentiumは32ビットコード向けを第一に設計されており、16ビットコードでの性能は劣る。コードとデータをセグメント分けすることも性能をはっきり劣化させるので、あなたは32ビットフラットモードを選ぶべきであり、このモードをサポートするオペレーティングシステム(例えばWindows 95、OS/2、あるいは32ビットDOSエクステンダ)を選ぶべきである。このマニュアルにでてくるコードの例は、特に指定がなければ32ビットフラットメモリモデルを仮定している。

5. アラインメント

RAM上のすべてのデータは下のように2、4、または8で割り切れる番地にアラインするべきである。
                  アラインメント     アラインメント
オペランドサイズ   PPlainとPMMX        PProとPII
--------------------------------------------------
1  (byte)              1                   1
2  (word)         番地 MOD 4 < 3           2
4  (dword)             4                   4
6  (fword)             4                   8
8  (qword)             8                   8
10 (tbyte)             8                  16
--------------------------------------------------
ミスアラインされたデータのアクセスは最低3クロックサイクル余計にかかる。キャッシュラインの境界をまたぐと、ペナルティーはさらに高くなる。

PPlainとPMMXで走らせるときにはコードをアラインする必要はないが、PProとPIIで最適な性能を得るために、サブルーチンやループの入口を16でアラインしてもよい。


6. キャッシュ

PPlainとPProはコード用に8KB、データ用に8KBのオンチップキャッシュ(レベル1キャッシュ)を持っている。PMMXとPIIはコード用に16KB、データ用に16KB持っている。レベル1キャッシュにあるデータはちょうど1クロックサイクルで読み書きできる。一方、キャッシュミスするとたくさんのクロックサイクルを消費する。だから、キャッシュを最も有効に使うためにそれがどう働くか理解することは重要である。

データキャッシュはそれぞれ32バイトのライン256個または512個から成る。キャッシュされていないデータ項目を読むたびに、プロセッサはキャッシュライン全体をメモリから読む。キャッシュラインは常に32で割り切れる物理番地にアラインされている。32で割り切れる番地から1バイト読んでしまえば、続く31バイトはほとんど追加コストなしで読み書きできる。互いに近く使われるデータ項目を32バイトのアラインされたメモリブロックにまとめることで、この利点を活用できる。もし、例えば二つの配列をアクセスするループがあるなら、二つの配列をインターリーブして一つの配列にすればよい。そうすると、いっしょに使われるデータをいっしょに格納できる。

もし配列や他のデータ構造のサイズが32バイトの倍数なら、なるべく32でアラインするべきである。

キャッシュはset-associativeである。その意味は、キャッシュラインには、任意のメモリ番地を割り当てられるわけではないということである。各キャッシュラインには7ビットのセット値があって、物理RAM番地のビット5から11とマッチする(ビット0〜4はキャッシュラインの32バイトを定義する)。PPlainとPProは、128セット値のそれぞれについて二つのキャッシュラインを持つため、どんなRAM番地も割り当てられる可能性のあるキャッシュラインは二つである。PMMXとPIIは四つある。

この結果、キャッシュは、番地のビット5〜11の値が同じなら、たかだか二つまたは四つの異なるデータブロックしか保持できない。二つの番地が同じセット値を持つかどうかは次の方法で決められる。各番地の下5ビットを0にし、32で割り切れる値を得よ。二つの切り捨てた番地の差が4096(=1000H)の倍数なら、二つの番地は同じセット値を持つ。

このことを次のコード片を使って例示しよう。ここでESIは32で割り切れる番地を保持しているとする。

AGAIN:  MOV  EAX, [ESI]
        MOV  EBX, [ESI + 13*4096 +  4]
        MOV  ECX, [ESI + 20*4096 + 28]
        DEC  EDX
        JNZ  AGAIN
ここで使われている三つの番地は、切り捨てた番地の差が4096の倍数なので、同じセット値を持つ。このループはPPlainとPProではたいへん悲惨なふるまいをする。ECXを読むとき、適当なセット値を持つ空きキャッシュラインがないので、プロセッサは二つのキャッシュラインのうち最近使われてないほう(EAXのために使われたもの)を採用し、そのキャッシュラインを [ESI + 20*4096] から [ESI + 20*4096 + 31] までのデータで満たしてECXを読む。次に、EAXを読むとき、EAXのための値を保持していたキャッシュラインは今は破棄されていることに気づく。それで、最も近くに使われたのでないキャッシュラインを採用し、それはEBXの値を保持しているものである。以下同様である。これではキャッシュミスしか起きず、ループは60クロックサイクルとかかかる。もし第3行を変更して
        MOV  ECX, [ESI + 20*4096 + 32]
とすれば、32バイト境界を越えたので、最初の2行と同じセット値ではなくなり、3つの番地のそれぞれに問題なくキャッシュラインを割り当てられる。ループは今は3クロックサイクルしかかからない(初回を除いて)。たいへん考慮に値する改良である!

データの番地が同じキャッシュ値かどうか決めるのは、特に異なるセグメントに散らばっているときは、たいへん難しいかもしれない。この種の問題を避ける最もよい方法は、プログラムの決定的に重要な部分で使われるすべてのデータをキャッシュより大きくない一つの連続したブロックか、キャッシュのサイズの半分以下の二つのブロック(例えば静的データで一つのブロック、スタック上のデータで一つのブロック)に入れることである。これでキャッシュラインはきっと最適に使われるようになるだろう。

コードの決定的に重要な部分が大きなデータ構造やランダムなデータ番地をアクセスするなら、すべてのよく使う変数(カウンタ、ポインタ、制御変数など)を一つの連続した4kバイト以下のブロックに入れて、ランダムなデータをアクセスするための、キャッシュラインの完全なセットがあるようにしたいかもしれない。たぶんサブルーチンの引数や戻り番地のためのスタックスペースは結局必要なので、最もよいのは、よく使う静的データをスタック上の動的変数にコピーし、変更があったものは決定的に重要なループの外でコピーし戻すことである。

レベル1キャッシュにないデータ項目を読むことは、キャッシュライン全体をレベル2キャッシュから満たすことになる。これはだいたい200ns(つまり100MHzシステムでは20クロック、200MHzシステムでは40クロック)かかるが、最初に読みたかったバイトは50〜100nsで利用可能になる。データ項目がレベル2キャッシュにもない場合、200〜300nsの遅れが生ずる。DRAMのページ境界をまたぐと、この遅れは多少長くなる(DRAMのページサイズは、4MBの72ピンRAMモジュールで1KB、16MBモジュールで2KBである)。

レベル1キャッシュにない番地に書いたときには、PPlainとPMMXでは、その値はそのままレベル2キャッシュかRAMに行く(レベル2キャッシュがどう設定されているかによる)。これはだいたい100nsかかる。もし8回かそれ以上同じ32バイトメモリブロックに書き、そこから読むことがなく、ブロックがレベル1キャッシュにないならば、そのブロックから最初にダミーの読み込みをしてキャッシュラインにロードするほうが有利かもしれない。同じブロックへの引き続く書き込みはすべて、キャッシュに行き、それは1クロックサイクルしかかからない。これは、書き込みミスで常にキャッシュラインをロードする、PProやPIIでは必要ない。PPlainとPMMXでは、同じ番地に繰り返し書き、その間に読まないと、ときどき小さなペナルティーがある。

書き込みミスを減らす別の方法は、整数のレジスタを使って4バイトを一度に書く代わりに、QWORDオペランドのFILD / FISTPを使って8バイトを一度に書くことである。遅いFILDとFISTP命令の余分なコストは書き込みが半分ですむことで補償される。しかし、この方法が有利なのはPPlainとPMMXだけであり、しかも転送先がレベル1キャッシュにない場合に限る。この方法は19章で説明する。PProまたはPIIを持っているなら、8バイトを一度に移動するには、もちろん浮動小数点命令ではなくMMX命令を使えばよい。

PPlainは二つの書き込みバッファを持っており、PMMXは四つ、PProとPIIは八つである。PMMXでは、キャッシュされていないメモリに対して最大四つまでの未完了の書き込みがあっても、引き続く命令を遅らせることはない。

スタック領域はキャッシュにあることがたいへん多いので、一時データはスタックに格納すると便利である。しかしながらDWORDサイズのスタックにQWORDデータを格納したり、WORDサイズのスタックにDWORDデータを格納する場合は、アラインメントの問題の可能性があることを認識するべきである。

もし二つのデータ構造の寿命の範囲が重ならない場合、キャッシュの効率を上げるために同じRAM領域を使うかもしれない。これは一時変数をスタックに割り付けるという日常習慣と整合性がある。

一時データをレジスタに格納することはもちろんもっと効率的である。レジスタは希少なリソースなので、スタックのデータをアクセスするのに[EBP]ではなく[ESP]を使い、EBPを他の目的のために空けたいかもしれない。ESPの値はPUSHやPOPをするたびに変化することを忘れないでほしい(16ビットWindowsでは、ESPを使うことはできない。タイマ割り込みがコード中の予測できない場所でESPの上位ワードを変更する)。

コード用には別のキャッシュがあり、それはデータキャッシュと似ている。コードキャッシュのサイズは、PPlainとPProで8KB、PMMXとPIIで16KBである。コードの決定的に重要な部分(最も内側のループ)がキャッシュに収まることは重要である。よく使われるコード片やいっしょに使われるルーチンはなるべく互いに近くに格納するべきである。めったに使われない分岐や手続きはコードの下のほうかどこか別の場所に離しておくべきである。


7. 番地生成インターロック(AGI) (PPlain and PMMX)

メモリをアクセスする命令で必要な番地の計算には1クロックサイクルかかる。普通はこの計算は、先立つ命令や命令ペアが実行されている間に、パイプラインの別のステージで行われる。しかしもし、番地が一つ前のクロックサイクルで実行された命令の結果に依存する場合は、番地の計算のために1クロックサイクル余分に待たなければならない。これはAGIストールと呼ばれる。
例:
ADD EBX,4 / MOV EAX,[EBX]    ; AGIストール
この例のストールは ADD EBX,4 と MOV EAX,[EBX] の間に何か他の命令をはさむか、コードを次のように書き換えることで取り除ける。
MOV EAX,[EBX+4] / ADD EBX,4

ESPを暗黙に番地指定に使う、PUSH、POP、CALL、RETのような命令でも、MOV、ADD、SUBのような命令で先立つクロックサイクル中にESPが変更された場合は、AGIストールが発生する。PPlainとPMMXはスタック操作の後のESPの値を予想する特別な回路を持つため、PUSH、POP、CALLでESPを変更した後のAGIによる遅れはない。RETの後のAGIストールは、ESPに足す即値を持つ場合に限ってある。

例:
ADD ESP,4 / POP ESI            ; AGIストール
POP EAX   / POP ESI            ; ストールなし、ペア
MOV ESP,EBP / RET              ; AGIストール
CALL L1 / L1: MOV EAX,[ESP+8]  ; ストールなし
RET / POP EAX                  ; ストールなし
RET 8 / POP EAX                ; AGIストール

LEA命令も、先立つクロックサイクルで変更された、ベースまたはインデックスレジスタを使う場合、AGIストールを受ける。

例:
INC ESI / LEA EAX,[EBX+4*ESI]  ; AGIストール

PProとPIIには、AGIストールはない。


8. 整数命令のペアリング(PPlain and PMMX)

PPlainとPMMXは、命令の実行のための二つのパイプライン、UパイプとVパイプを持つ。ある条件の元で、二つの命令を同時に、一つはUパイプで、もう一つはVパイプで実行できる。これはほとんど実行速度を倍にする。そのため、命令を並べ変えてペアにするのは有益である。 次の命令はどちらのパイプでもペアにできる。
MOV レジスタ、メモリ、または即値を、レジスタ、またはメモリへ
PUSH レジスタ、または即値
POP レジスタ
LEA, NOP
INC, DEC, ADD, SUB, CMP, AND, OR, XOR
TESTのいくつかの形式(段落17.3を参照)
次の命令はUパイプでのみペアにできる。
ADC, SBB
SHR, SAR, SHL, SAL 回数は即値
ROR, ROL, RCR, RCL 回数は即値の1
次の命令はどちらのパイプでも実行できるが、ペアにできるのはVパイプの時だけである。
near call, shortまたはnearジャンプ, shortまたはnear条件ジャンプ
他のすべての整数命令はUパイプでのみ実行可能であり、ペアにできない。

連続する二つの命令は次の条件が満たされたときペアにできる。

8.1
最初の命令はUパイプでペアにでき、二番目の命令はVパイプでペアにできる。
8.2
二番目の命令は最初の命令が書くレジスタを読み書きできない。
例:
    MOV EAX, EBX / MOV ECX, EAX     ; 書き込み後読み込み、ペアにできない
    MOV EAX, 1   / MOV EAX, 2       ; 書き込み後書き込み、ペアにできない
    MOV EBX, EAX / MOV EAX, 2       ; 読み込み後書き込み、ペアOK
    MOV EBX, EAX / MOV ECX, EAX     ; 読み込み後読み込み、ペアOK
    MOV EBX, EAX / INC EAX          ; 読み込み後読み書き、ペアOK
8.3
規則8.2で部分レジスタはレジスタ全体として扱われる。
例:
    MOV AL, BL  /  MOV AH, 0        ; 同じレジスタの異なる部分への書き込み
                                    ; ペアにできない
8.4
規則8.2と8.3にかかわらず、フラグレジスタの一部に書き込む二つの命令はペアにできる。
例:
    SHR EAX,4 / INC EBX             ; ペアOK
8.5
規則8.2にかかわらず、フラグに書き込む命令と条件ジャンプはペアにできる。
例:
    CMP EAX, 2 / JA LabelBigger     ; ペアOK
8.6
次の命令の組合せは、両方がスタックポインタを変更するという事実にもかかわらず、ペアにできる。
    PUSH + PUSH,  PUSH + CALL,  POP + POP
8.7
プリフィックスつきの命令のペアリングには制限がある。プリフィックスにはいくつかの種類がある。

PPlainでは、プリフィックスつき命令は、near条件ジャンプを除いてUパイプでのみ実行可能である。

PMMXでは、オペランドサイズ、アドレスサイズ、0FHのプリフィックスつき命令は、どちらのパイプでも実行可能であるが、一方、セグメント、リピート、ロックプリフィックスつき命令はUパイプでしか実行できない。

8.8
変位と即値の両方を持つ命令は、PPlainではペアにできず、PMMXではUパイプでのみ実行可能である。
    MOV DWORD PTR DS:[1000], 0    ; ペアにできないかUパイプのみ
    CMP BYTE PTR [EBX+8], 1       ; ペアにできないかUパイプのみ
    CMP BYTE PTR [EBX], 1         ; ペアにできる
    CMP BYTE PTR [EBX+8], AL      ; ペアにできる
(PMMXにおける、変位と即値の両方を持つ命令の別の問題は、そのような命令は7バイトより長くなるかもしれないことで、それは、13章で説明するように、1クロックサイクルで1命令しかデコードできないことを意味する。)
8.9
両方の命令があらかじめロードされ、デコードされている。これは9章で説明されている。
8.10
PMMXのMMX命令には特別なペアリング規則がある。

9. 初回の実行と繰り返し実行

コード片を最初に実行するときは、繰り返すときに比べて、普通はたいへん時間がかかる。理由は次の通りである。
9.1
コードをRAMからキャッシュにロードするのは、実行するより時間がかかる。
9.2
ある場合にはコードのデコードがボトルネックになる。命令の長さを決めるのに1クロックサイクルかかるなら、プロセッサは二番目の命令がどこから始まるかわからないので、クロックサイクルあたり二つの命令をデコードすることはできない。Pentiumはこの問題を、キャッシュに残っている命令の長さを前回実行したときから覚えておくことで、解決している。この結果、PPlainとPMMXでは、二つの命令の前者の長さが1バイトの場合を除いて、初回には命令をペアにできない。
9.3
初回の実行では、ジャンプ命令はブランチターゲットバッファに入っていないので、正しく予測されないことが多い。12章を参照のこと。
9.4
コードがアクセスするどのデータもキャッシュにロードしなければならず、それは命令の実行よりずっと時間がかかるかもしれない。コードが繰り返されると、データはキャッシュにあることが多い。

この四つの理由のため、ループ中のコード片が最初に実行されるときには、次回以降より一般にずっと時間がかかる。

コードキャッシュに収まらないようなループがあると、キャッシュから実行されることはないので、9.1と9.2のペナルティーを毎回に受けることになる。だから、ループを再構成してキャッシュに収まるように務めるべきである。

ループ中にジャンプやブランチが多い場合、9.3のペナルティーを毎回に受けるかもしれない。

同様に、データキャッシュに取って大きすぎるデータをループが繰り返しアクセスする場合、毎回キャッシュミスのペナルティーを受ける。


10. 不完全なペアリング

ペアの二つの命令が同時に実行されなかったり、時間的に一部だけオーバーラップしたりする状況がある。しかし、最初の命令がUパイプで、二番目の命令がVパイプで実行されるので、これも依然としてペアとして考慮するべきである。不完全なペアの両方の命令の実行が完了しないと、引き続く命令の実行は始まらない。

不完全なペアリングは次のような場合に起きる。

10.1
二番目の命令がAGIストールを受ける場合(7章参照)。
10.2
二つの命令はメモリの同じDWORDを同時にアクセスできない。次の例はESIが4で割り切れると仮定している。
     MOV AL, [ESI] / MOV BL, [ESI+1]
二つのオペランドは同じDWORD内にあるので、同時には実行できない。このペアは2クロックサイクルかかる。
     MOV AL, [ESI+3] / MOV BL, [ESI+4]
ここでは二つのオペランドはDWORD境界の両側にあるので、完全にペアになれ、1クロックサイクルしかかからない。
10.3
規則10.2は二つの番地のビット2〜4が同じである場合に拡張される(キャッシュバンク競合)。DWORDの番地に対しては、これは二つの番地の差が32で割り切れてはならないことを意味する。
例:
     MOV [ESI], EAX / MOV [ESI+32000], EBX ;  不完全なペアリング
     MOV [ESI], EAX / MOV [ESI+32004], EBX ;  完全なペアリング

ペアにできる整数命令で、メモリにアクセスしないものは、予測ミスしたジャンプを除いて、実行に1クロックサイクルかかる。メモリから、またはメモリへのMOV命令は、データ領域がキャッシュにあって適当にアラインされていれば、やはり1クロックサイクルしかかからない。スケールされたインデックスレジスタのような複雑な番地指定モードの使用に速度のペナルティーはない。

ペアにできる整数命令で、メモリから読み、何らかの計算をし、結果をレジスタやフラグに格納するものは、2クロックサイクルかかる。

ペアにできる整数命令で、メモリから読み、何らかの計算をし、結果をメモリに書き戻すものは、3クロックサイクルかかる(read/modify/write命令)。

10.4
もし、read/modify/write命令がread/modify命令またはread/modify/write命令とペアになると、それは不完全なペアリングである。

消費するクロックサイクル数は次の表のようになる。

                      |                 二番目の命令
                      | MOV or             read/       read/modify/
最初の命令            | register only      modify      write
----------------------|----------------------------------------------
MOV or register only  |      1               2              3
read/modify           |      2               2              3
read/modify/write     |      3               4              5
----------------------|-----------------------------------------------
例:
ADD [mem1], EAX / ADD EBX, [mem2]  ; 4クロックサイクル
ADD EBX, [mem2] / ADD [mem1], EAX  ; 3クロックサイクル
10.5
ペアになった二つの命令が両方とも、キャッシュミス、ミスアラインメント、または分岐予測ミスによって余分な時間がかかるとき、そのペアは各命令単独よりは時間がかかるが、二つの和よりは少ない。
10.6
ペアにできる浮動小数点命令にFXCH命令が続いているものは、その次の命令が浮動小数点命令でなければ、不完全なペアリングとなる。

不完全なペアリングを避けるためには、どの命令がUパイプに、どの命令がVパイプに行くかを知らなければならない。これは、次のようにすればわかる。コードを逆方向に見て行って、ペアにできない、一方のパイプでしかペアにできない、または8章の規則のどれかのためにペアにできない命令をさがせばよい。

不完全なペアリングはたいてい、命令の順序を変更することで避けられる。

例:
L1:     MOV     EAX,[ESI]
        MOV     EBX,[ESI]
        INC     ECX
ここで二つのMOV命令は同じメモリ位置をアクセスするので、不完全なペアを形成する。この命令列は3クロックサイクルかかる。命令の順番を変えて、 INC ECX がMOV命令のどちらかとペアになるようにすれば、改良できる。
L2:     MOV     EAX,OFFSET A
        XOR     EBX,EBX
        INC     EBX
        MOV     ECX,[EAX]
        JMP     L1
ペア INC EBX / MOV ECX,[EAX] は、後者の命令にAGIストールがあるため、不完全である。この命令列は4クロックかかる。NOPまたは他の命令を挿入して、 MOV ECX,[EAX] が代わりに JMP L1 とペアになるようにすれば、命令列は3クロックしかかからない。

10.7

次の例は16ビットモードで、SPが4で割り切れると仮定する。

L3:     PUSH    AX
        PUSH    BX
        PUSH    CX
        PUSH    DX
        CALL    FUNC
ここでPUSH命令は二つの不完全なペアを形成する。なぜなら、各ペアの両方のオペランドがメモリの同じDWORDに行くからである。 PUSH BX は PUSH CX と完全なペアになれたかもしれない(DWORD境界の両側に行くから)のに、すでに PUSH AX とペアになってしまっているので、そうはならない。命令列は、従って、5クロックサイクルかかる。もしNOPか他の命令を挿入して、 PUSH BX が PUSH CX と、 PUSH DX が CALL FUNC とペアになるようにすれば、命令列は3クロックしかかからない。問題を解決する別の方法は、SPが必ず4で割り切れないようにすることである。16ビットモードでSPが4で割り切れるかどうか知るのは困難なので、この問題を避ける最もよい方法は、32ビットモードを使うことである。

11. 複雑な命令を単純な命令に分割(PPlain and PMMX)

read/modifyまたはread/modify/write命令を分割して、ペアリングを改良してもよい。
例:
ADD [mem1],EAX / ADD [mem2],EBX    ; 5クロックサイクル
このコードは3クロックサイクルしかかからない命令列に分割できる。
MOV ECX,[mem1] / MOV EDX,[mem2]
ADD ECX,EAX / ADD EDX,EBX
MOV [mem1],ECX / MOV [mem2],EDX

同様に、ペアにできない命令を、ペアにできる命令に分割してもよい。

PUSH [mem1] / PUSH [mem2]  ; ペアにできない
これを分割して
MOV EAX,[mem1] / MOV EBX,[mem2] / PUSH EAX / PUSH EBX  ; すべてペアになる

ペアにできない命令で、より単純なペアにできる命令に分割できる、他の例:

CDQ を分割して MOV EDX,EAX / SAR EDX,31
NOT EAX の代わりに XOR EAX,-1
NEG EAX を分割して XOR EAX,-1 / INC EAX
MOVZX EAX,BYTE PTR [mem] を分割して XOR EAX,EAX / MOV AL,BYTE PTR [mem]
JECXZ を分割して TEST ECX,ECX / JZ
LOOP  を分割して DEC ECX / JNZ
XLAT  の代わりに MOV AL,[EBX+EAX]

もし命令を分割することで速度が改良されなければ、コードサイズを縮小するために、複雑な、またはペアにできない命令をそのままにしてもよい。命令の分割は、PProとPIIでは必要ない。


12. ジャンプとブランチ

Pentiumファミリ・プロセッサは、ジャンプの先がどこか、そして条件ジャンプが分岐するか通り抜けるかを予測しようとする。予測が正しければ、ジャンプが実行される前に、引き続く命令をパイプラインにロードして、デコードを始めることで、考慮に値するべき時間を節約できる。予測が間違っていることがわかれば、パイプラインをフラッシュしなければならず、パイプラインの長さによって決まるペナルティーとなる。

予測は、各分岐またはジャンプ命令の履歴を格納して、各命令の以前の実行履歴から予測を行う、 Branch Target Buffer (BTB)に基づいて行われる。BTBは、新しいエントリが擬似ランダム置換方式で割り当てられるようなset-associativeキャッシュと似た構成になっている。

コードを最適化するとき、予測ミスのペナルティーの数を最小にすることが重要である。これには、分岐予測がどのように働くかよく理解することが要求される。

分岐予測機構についてはインテルのマニュアルにも、また他のどこでも正確には書かれていない。だから私は、たいへん詳細な記述をここで与える。この情報は私自身の調査に基づいている(PPlainでは Karki Jitendra Bahadur の助けもあった)。

以下で私は、「制御移行命令」という言葉を、IP(instruction pointer)を変更するどんな命令についても、条件つき、無条件、直接、間接、near、far、ジャンプ、コール、リターンを含めて、使うことにする。これらすべての命令が予測を使う。

12.1.1 PPlainの分岐予測

PPlainの分岐予測機構は、他の3プロセッサとだいぶ異なる。この題目についてのインテルの文書や他の場所で見られる情報は、一直線に人を誤解させるもので、そのような文書の忠告に従うと、準最適なコードになってしまう。

PPlainは branch target buffer (BTB)を持ち、それは256個までのジャンプ命令の情報を保存できる。BTBはウェイあたり64エントリの4ウェイset-associativeキャッシュと似た構成になっている。これの意味するところは、BTBは同じセット値を持つエントリをたった4つしか保持できないということである。データキャッシュと違って、BTBは擬似ランダム置換アルゴリズムを使っており、最近使われてないほうの、同じセット値のエントリを置き換えるわけでは必ずしもない。セット値がどのように計算されるかは後で述べる。各BTBエントリはジャンプ先の番地と、4つの異なる値をとる予測の状態を格納する。

状態0: 強 分岐しない
状態1: 弱 分岐しない
状態2: 弱 分岐する
状態3: 強 分岐する
分岐命令は状態2と3ではジャンプすると、0と1では通り抜けると予測される。状態遷移は2ビットカウンタのように働き、分岐すると状態は1増え、通り抜けると1減る。カウンタはラップアラウンドせず飽和するので、0を超えて減ったり、3を超えて増えたりしない。理想的には、これはまずまずよい予測になるだろう。なぜなら、予測が変化する前に分岐命令は、よくする動作から2回はずれなければならないからである。

しかし、状態0が「使われていないBTBエントリ」も意味するという事実により、この機構には妥協がある。だから、状態0のBTBエントリは、BTBエントリがないのと同じである。分岐命令はBTBエントリを持たない場合通り抜けると予測されるので、これは筋が通っている。ほとんど分岐しない分岐命令はほとんどの時間、BTBエントリの場所を取らないので、これはBTBの使用効率を改良する。

ここで、もしジャンプする命令がBTBエントリを持たなければ、新しいBTBエントリが生成され、これはいつでも状態3にセットされる。これは、状態0から状態1に行くのは不可能であることを意味する(後に議論する非常に特別な場合を除く)。状態0からは、分岐した場合は状態3にしか行けない。分岐が通り抜けた場合、BTBにはいらないままになる。

これは深刻な、設計の欠陥である。状態0のエントリを捨て、新しいエントリをいつでも状態3にセットすることで設計者は、無条件ジャンプやよく分岐する条件ジャンプの初回のペナルティーを少なくすることを優先し、これが機構の背後にある基本アイデアに対して重大な妥協をしていて、最も内側の小さいループの性能を落としていることを無視したようだ。この欠陥の帰結は、たいてい通り抜ける分岐命令は、たいてい分岐する分岐命令の3倍も予測ミスがあるということである。

この非対称性を考慮に入れて、分岐命令は分岐するほうが多くなるように構成するとよい。

例えばこのif-then-else構造を考えてみよう。

        TEST EAX,EAX
        JZ   A
        <枝1>
        JMP  E
A:      <枝2>
E:
もし枝1が枝2より多く実行され、枝2が続けて2回実行されることがめったになければ、二つの枝を交換して、分岐命令が通り抜けるよりジャンプするほうが多いようにすることで、分岐予測ミスを3分の1にまで減らすことができる。
        TEST EAX,EAX
        JNZ  A
        <枝2>
        JMP  E
A:      <枝1>
E:
(これは、インテルのマニュアルやチュートリにある勧めとはである。分岐予測の非対称性に気づいていないようである。しかし、後者の例が優れていることを示すのはたやすい。)

だが、最もよく実行される枝を最初に置く理由もあるかもしれない。

  1. めったに実行されない枝をコードの最後に置くことで、コードキャッシュの使用効率を上げることができる。
  2. めったに分岐しない分岐命令はたいていはBTBにはいらずにいて、BTBの使用効率を上げる可能性がある。
  3. 他の分岐命令によってBTBから追い出されてしまった分岐命令は、分岐しないと予測される。
  4. 分岐予測の非対称性は、PPlainにだけある。

これらの考慮は、しかしながら、小さく決定的に重要なループに関してはウェイトは小さいので、やはり、分布の偏った枝の構成は、分岐命令が分岐するほうが多いようにすることを勧める。ただし、枝2の実行頻度があまりに小さくて、予測ミスが問題にならないときを除く。

同様に、ループの条件テストの分岐命令は、この例のように、なるべく最後に来るように構成するべきである。

        MOV ECX, [N]
L:      MOV [EDI],EAX
        ADD EDI,4
        DEC ECX
        JNZ L
もしNが大きければ、JNZ命令は分岐するほうが多く、続けて2回通り抜けることはない。

1回おきに分岐が起きる状況を考えてみよう。最初にジャンプしたとき、BTBエントリは状態3に行き、その後状態2と3に交互に行くだろう。これはいつもジャンプすると予測され、50%の予測ミスとなる。今これがこの規則的なパターンからはずれ、1回余計に通り抜けたとしよう。ジャンプのパターンは、0がとばない、1がとぶの意味だとして、

01010100101010101010101
       ^
増えた通り抜けは^で示してある。このできごとの後では、BTBエントリは状態1と2に交互に行き、100%の予測ミスとなる。この不運なモードはまた0101パターンからはずれるまで続く。これがこの分岐予測機構で最も悪い場合である。

12.1.2 BTBは先読みをしている(PPlain)

BTB機構は、命令を単独ではなくペアで数えているので、BTBのエントリがどこに格納されるかを解析するには、命令がどのようにペアになるのか知らなければならない。どの制御移行命令のBTBエントリも、その前の命令ペアのUパイプの命令の番地につく(ペアにならない命令も一つのペアと数える)。
例:
        SHR EAX,1
        MOV EBX,[ESI]
        CMP EAX,EBX
        JB  L
ここで、SHRはMOVと、CMPはJBとペアになる。従って、 JB L についてのBTBエントリは、 SHR EAX,1 命令の番地につく。このBTBエントリに出会ったとき、それが状態2か3なら、PentiumはBTBエントリから分岐先の番地を読み、ラベルL以降の命令をパイプラインにロードする。これは分岐命令がデコードされる前に起きるので、PentiumはこれをするときBTBの情報だけに頼っている。

命令は、初回に実行されるときにはめったにペアにならないことを、ここで思い出すかもしれない(段落9.2参照)。上の命令がペアにならなければ、BTBエントリはCMP命令の番地につくであろう。そして、次回の実行で命令がペアになるときには、このエントリは誤りになるだろう。しかしながら、多くの場合、PPlainは十分賢く、ペアになる機会を利用しなかったものがあるときには、BTBエントリを作らないので、2回目の実行まではBTBエントリはできず、だから、3回目の実行までは予測はされないだろう(一つおきに1バイト命令があるような、稀な場合では、2番目の実行では無効になるようなBTBエントリが最初の実行でできるかもしれないが、それならエントリのついている命令はVパイプに行くので、それは無視されてペナルティーを与えない。BTBエントリが読まれるのは、それがUパイプの命令の番地についているときだけである)。

BTBエントリは、それがつく番地のビット0〜5に等しいセット値で見分けられる。ビット6〜31はタグとしてBTBに格納される。64の倍数だけ離れた番地は同じセット値を持つ。同じセット値を持つBTBエントリは4つまでしか作れない。制御移行命令のセット値が競合するかどうかチェックしたいなら、前の命令ペアのUパイプ命令のアドレスのビット0〜5を比べなければならない。これはたいへんあきあきするし、誰かがやっていると聞いたこともない。この仕事をあなたの代わりにやってくれるような、利用可能なツールはない。

12.1.3 連続する分岐(PPlain)

ジャンプが予測ミスすると、パイプラインはフラッシュされる。もし、次に実行される命令ペアも制御移行命令を含んでいると、PPlainはジャンプ先をロードしないのである。なぜなら、パイプラインのフラッシュ中に新しいジャンプ先をロードできないからである。この結果、二番目の制御移行命令はBTBエントリの状態に関係なく、通り抜けると予測される。それで、二番目の制御移行命令も分岐するなら、さらにペナルティーを受ける。二番目の制御移行命令のBTBエントリの状態は、それでも正しく更新される。もしジャンプする制御移行命令の長い鎖があって、鎖の最初のジャンプが予測ミスしたら、パイプラインは毎回フラッシュされ、ジャンプしない命令ペアに出会うまでずっと予測ミスしか起きない。これの最も極端な場合は、自分自身にジャンプするループであり、繰り返し毎に予測ミスのペナルティーを受ける。

連続する制御移行命令の問題はこれだけではない。別の問題は、BTBエントリと、それが属する制御移行命令の間に、もう一つの分岐命令を置けることである。最初の分岐命令がどこか別の場所にジャンプすると、奇妙なことが起きるかもしれない。この例を考えてみよう。

        SHR EAX,1
        MOV EBX,[ESI]
        CMP EAX,EBX
        JB  L1
        JMP L2

L1:     MOV EAX,EBX
        INC EBX
JB L1 が通り抜けるとき、 CMP EAX,EBX の番地につけられた、 JMP L2 のためのBTBエントリができる。しかし、後で JB L1 が分岐したとき何が起きるだろうか。 JMP L2 のためのBTBエントリが読まれるとき、プロセッサは次の命令ペアが制御移行命令を含んでいないことを知らないので、実際には命令ペア MOV EAX,EBX / INC EBX がL2にジャンプすると予測する。ジャンプでない命令がジャンプすると予測したときのペナルティーは3クロックサイクルである。 JMP L2 のためのBTBエントリは、何かジャンプしないものに適用されたために、その状態が1減らされる。もしL1に行き続けるなら、 JMP L2 のためのBTBエントリは状態1、そして0まで減らされて、次に JMP L2 が実行されるまでこの問題は姿を消すだろう。

ジャンプでない命令をジャンプすると予測するペナルティーは、L1へのジャンプが予測されたときのみ発生する。 JB L1 が予測とはずれてジャンプする場合は、パイプラインがフラッシュされ、間違ったジャンプ先のL2はロードされないので、ジャンプでない命令をジャンプすると予測したペナルティーは見えないが、 JMP L2 のBTBエントリの状態はやはり減らされる。

今、 INC EBX 命令を別の制御移行命令で置き換えてみよう。この三番目の制御移行命令は JMP L2 命令と同じBTBエントリを使い、誤ったジャンプ先を予測するペナルティーを受ける可能性がある(たまたまジャンプ先が同じL2である場合を除いて)。

要約すると、連続するジャンプは、次のような問題につながる可能性がある。

この混乱はすべて、たくさんのペナルティーを与えるので、うまく予測できない制御移行命令のすぐ後またはジャンプ先に、ジャンプを含む命令ペアを置くことは、絶対に避けるべきである。

そろそろこれを説明する例を挙げるころである。

        CALL P
        TEST EAX,EAX
        JZ   L2
L1:     MOV  [EDI],EBX
        ADD  EDI,4
        DEC  EAX
        JNZ  L1
L2:     CALL P
これはなかなかよい、普通のコード片に見える。関数呼び出しと、回数が0のときは迂回されるループと、別の関数呼び出しである。あなたはこのプログラムにいくつの問題を見つけ出せるだろうか。

まず、関数Pが交互に二つの異なる場所から呼ばれることに注意しよう。これはPからの戻り先が毎回変わることを意味する。その結果、Pからのリターンはいつも予測ミスする。

今、EAXが0だと仮定しよう。すると、予測ミスしたPからのリターンはパイプラインフラッシュを起こしているので、L2へのジャンプはそのジャンプ先をロードしない。次に、 JZ L2 がパイプラインフラッシュを起こしたので、二番目の CALL P も分岐先のロードに失敗する。これが、最初のジャンプが予測ミスしたために、連続したジャンプの鎖が繰り返しパイプラインフラッシュを引き起こす状況である。 JZ L2 のためのBTBエントリはPのリターン命令の番地に格納される。このBTBエントリは二番目の CALL P の後に来るものが何であっても誤って適用されるが、予測ミスした二番目のリターンによってパイプラインがフラッシュされるので、ペナルティーを与えない。

今度は、次回、EAXが0以外の値だったら何が起きるか見てみよう。フラッシュによって JZ L2 はいつでも通り抜けると予想される。二番目の CALL P は TEST EAX,EAX の番地にBTBエントリを持つ。このエントリはMOV/ADDのペアに誤って適用され、Pにジャンプすると予測するだろう。これはフラッシュを起こし、 JNZ L1 がそのジャンプ先をロードするのを妨げる。もし以前ここに来たことがあるのなら、二番目の CALL P は DEC EAX の番地にもう一つのBTBエントリを持つ。ループの2,3回目の繰り返しにおいて、このエントリもMOV/ADDのペアに誤って適用される(状態が1か0に減らされるまで)。2回目の繰り返しでは、 JNZ L1 によるフラッシュが誤ったジャンプ先のロードを止めるので、これはペナルティーを起こさないが、3回目の繰り返しではペナルティーを起こす。引き続くループの繰り返しではペナルティーはないが、出るときに、 JNZ L1 が予測ミスする。CALL P のためのBTBエントリが、何回か誤って適用されたために、すでに破壊されているという事実がなければ、これによるフラッシュは、今度は CALL P がジャンプ先をロードするのを妨げただろう。

すべての連続するジャンプを分けるためにいくつかNOPを入れることで、このコードを改良できる。

        CALL P
        TEST EAX,EAX
        NOP
        JZ   L2
L1:     MOV  [EDI],EBX
        ADD  EDI,4
        DEC  EAX
        JNZ  L1
L2:     NOP
        NOP
        CALL P
余分なNOPは2クロックサイクルかかるが、ずっと多くを節約する。さらに、 JZ L2 はUパイプに移動し、予測ミスしたときのペナルティーが4から3に減っている。残る唯一の問題は、Pからのリターンがいつも予測ミスすることである。この問題はPの呼び出しをインラインマクロで置き換えることによってのみ解決できる(もしコードキャッシュが十分なら)。

この例から学ぶべき教訓は、いつも注意深く、連続するジャンプを探して、NOPをいくつか挿入すると時間を節約できるかどうか考えるべきであるということである。ループの出口やいろいろな場所から呼ばれる手続きからのリターンなど、予測ミスが避けられない状況を特に認識するべきである。NOPの代わりに挿入する有効なものがあるのなら、もちろんそうするべきである。

多方向分岐(case文)は、分岐命令の木か、ジャンプする番地のリストで実装されるだろう。分岐命令の木を使うことを選ぶのなら、連続する分岐を分けるためにNOPかほかの命令を含めなければならない。PPlainでは従って、ジャンプする番地のリストがよい解かもしれない。ジャンプする番地のリストはデータセグメントに置くべきである。決してデータをコードセグメントに置くな!

12.1.4 複数のBTBエントリ(PPlain)

二つの制御移行命令は同じBTBエントリを共有し得るが、一方、一つの制御移行命令が、異なる場所からジャンプしてくるか、ペアリングが変化した場合に、複数のBTBエントリを持つこともあり得る。
例:
        JZ  L1
        MOV EAX,1
L1:     MOV EBX,2
        MOV ECX,3
        JC  L2
もし、 JZ L1 が通り抜けると、 MOV EAX,1 は MOV EBX,2 とペアになり、 MOV ECX,3 は JC L2 とペアになる。 JC L2 のためのBTBエントリは MOV EAX,1 の番地につくことになる。 JZ L1 が分岐したときには、 MOV EBX,2 は MOV ECX,3 とペアになり、 JC L2 はペアにならず、BTBエントリは MOV EBX,2 につくことになる。このように JC L2 には二つのBTBエントリができる。最初のエントリはゼロフラグが降りているとき、もう一つは立っているときに使われる。これは実は好都合かもしれない。なぜなら、二つの分岐命令に相関があるなら、二番目の分岐命令の予測可能性を向上させるからである。例えば、このコード列の前にCMP命令があると仮定しよう。すると、ゼロフラグとキャリーフラグが両方同時に立つことは決してないと確信できる。ゼロフラグが立っていると、 JC L2 は必ず通り抜けるので、 MOV EBX,2 に JC L2 のための二番目のBTBエントリはつかず、 JZ L1 が分岐したときは JC L2 の予測ミスはない。このトリックを利用できる状況は、しかしながら稀である。というのは、L1をキャリーフラグを全くテストしない別の枝に持って行ったほうがましだからである。

12.1.5 きついループ(PPlain)

小さなループでは、同じBTBエントリを短い間隔で繰り返しアクセスするものである。これは決してストールを起こさない。BTBエントリが更新されるのを待つのではなく、PPlainは何らかの方法でパイプラインをバイパスし、前のジャンプの結果の状態を、それがBTBに書かれる前に取得する。この機構は利用者にはほとんど透過であるが、ある場合におかしな効果がある。分岐予測が状態0から状態3ではなく、状態1に遷移するのが、状態0がまだBTBに書かれていないなら、見られる。ループがたった4命令ペアしかないと、これが起きる。2命令ペアしかないループではときどき、連続した2回の繰り返しで状態0がBTBから出て行かないかもしれない。そのような小さなループでは、前のではなく2回前の繰り返しの結果の状態を予測が使うということが、稀な場合に起きる。これらのおかしな効果は、通常は性能に負の効果をもたらさない。

12.2 PMMX、PPro、PIIの分岐予測

12.2.1 BTBの構成(PMMX, PPro and PII)

PMMXの branch target buffer (BTB)は、256個のエントリを持ち、それは16ウェイ×16セットの構成になっている。各エントリは、それの属する制御移行命令の最後のバイトの番地のビット2〜31で特定される。ビット2〜5がセットを定義し、ビット6〜31がタグとしてBTBに格納される。64バイト離れた制御移行命令どうしは同じセット値を持つので、時には互いに相手をBTBから押し出したりする。セット毎に16のウェイがあるので、これはそんなには起きないだろう。

PProとPIIの branch target buffer は512個のエントリを持つ。これがどのようなセットで構成されているかの情報を私は持っていない。

PProとPIIはどんな制御移行命令でもそれが最初に実行されたときにBTBエントリを割り当てる。PMMXはそれが最初にジャンプしたときに割り当てる。PMMXでは、決してジャンプしない分岐命令はBTBには入らずにいる。そして一度ジャンプしたら、もう決して再びジャンプしなくても、それはBTBにとどまるのである。

エントリは、同じセット値を持つ別の制御移行命令がBTBエントリを必要としたときに、BTBから押し出されることもある。

12.2.2 予測ミスのペナルティー(PMMX, PPro and PII)

PMMXでは、条件ジャンプの予測ミスのペナルティーは、Uパイプで4クロック、Vパイプで実行されたときは5クロックである。他のすべての制御移行命令では、4クロックである。

PProとPIIでは、長いパイプラインのせいで、予測ミスのペナルティーは非常に高い。予測ミスは普通、10〜20クロックサイクルかかる。そのため、PProとPIIで走らせるときには、予測のうまくいかない分岐を意識しておくことは非常に重要である。

12.2.3 条件ジャンプのパターン認識(PMMX, PPro and PII)

これらのプロセッサは、例えば、4回おきに分岐して残りの3回は通り抜けるような分岐命令を正しく予測できる、進んだパターン認識機構を持っている。実は、周期が高々5までのジャンプする/しないのどんな繰り返しパターンをも、そしてそれより長い周期の多くのパターンを予測できる。

このメカニズムは、 T.-Y. Yeh と Y. N. Patt の発明した、いわゆる「2レベル適応型分岐予測スキーム」である。これはPPlainについて上で説明したのと同種の(ただし非対称性の欠陥はない)2ビットカウンタに基づいている。カウンタはジャンプが分岐したときは増加し、分岐しなかったときは減少する。3より上、0より下にラップアラウンドはしない。対応するカウンタが状態2か3のときは、分岐命令は分岐すると予測され、状態0か1のときは、通り抜けると予測される。今、各BTBエントリに対してこのようなカウンタを16個持つことで、劇的な改良が得られる。その分岐命令の最後の4回の実行の履歴に基づいて16個のカウンタのうちの一つが選ばれる。例えば、分岐命令が一度ジャンプし、3回通り抜けたらなら、履歴ビットとして1000(1=ジャンプした、0=ジャンプしなかった)を持つ。これはカウンタ8(1000は2進数としてみると8)を次回の予測として使い、その後カウンタ8を更新する。

もし列1000の後にくるのがいつも1なら、カウンタ8はすぐに一番上の状態(状態3)に達し、1000の後はいつも1が来るだろうと予測する。予測が変化するには、このパターンから2回はずれる必要がある。繰り返しパターン100010001000は、カウンタ8を状態3に、カウンタ1,2,4を状態0にし、他の12個のカウンタは使わない。

12.2.4 完全に予測できるパターン(PMMX, PPro and PII)

以下は完全に予測できる繰り返しの分岐パターンのリストである。
周期       パターン
-----------------------------------------------------------------------------
1 - 5      すべて
6          000011, 000101, 000111, 001011
7          0000101, 0000111, 0001011
8          00001011, 00001111, 00010011, 00010111, 00101101
9          000010011, 000010111, 000100111, 000101101
10         0000100111, 0000101101, 0000101111, 0000110111, 0001010011,
           0001011101
11         00001001111, 00001010011, 00001011101, 00010100111
12         000010100111, 000010111101, 000011010111, 000100110111,
           000100111011
13         0000100110111, 0000100111011, 0000101001111
14         00001001101111, 00001001111011, 00010011010111, 00010011101011
           00010110011101, 00010110100111
15         000010011010111, 000010011101011, 000010100110111, 000010100111011
           000010110011101, 000010110100111, 000010111010011, 000011010010111
16         0000100110101111, 0000100111101011, 0000101100111101,
           0000101101001111
-----------------------------------------------------------------------------

この表を読むとき、次のことを知っているべきである。あるパターンが正しく予測できるなら、同じパターンを反転したもの(後ろ向きに読んだもの)も、また、同じパターンの全ビットを反転したものも正しく予測できる。

例:
表にはこのパターンがある: 0001011
パターンを反転すると:     1101000
全ビットを反転すると:     1110100
両方同時にやると:         0010111
これら四つのパターンはすべて認識できる。パターンを一つ左に回転すると、0010110になる。これはもちろん新しいパターンではなく、同じパターンの相がずれたものである。表の中のあるパターンから反転したり、ビット反転したり、回転したりして導出できるパターンもすべて認識できる。簡潔にするために、これらはリストされていない。(リストはPMMXで実験的に得られた)。

BTBエントリが割り当てられた後、パターン認識機構が規則的な繰り返しパターンを学習するのに2周期かかる。学習期間での予測ミスのパターンには再現性がない。これはたぶん、BTBエントリには割り当てに先立って何かが入っているからだろう。BTBエントリはランダムに割り当てられるので、最初の学習期間の間に何が起きているか予測できる可能性はほとんどない。

12.2.5 規則的なパターンからのはずれの扱い(PMMX, PPro and PII)

分岐予測機構は「ほとんど規則的な」パターンや、規則的なパターンからのはずれを扱うのも得意である。分岐予測機構は、規則的なパターンがどのように見えるか学習するだけではない。規則的なパターンからのはずれがどのように見えるかも学習する。もしはずれ方がいつも同じなら、不規則なできごとの後で何が来るかを覚え、はずれのコストは予測ミス1回だけですんでしまうのである。
例:
0001110001110001110001011100011100011100010111000
                      ^                   ^
この列で0はジャンプしない、1はジャンプすることを表す。分岐予測機構は繰り返される列が000111であることを学習する。最初の不規則性は、^でしるしをつけた、予期できない0である。0010,0101,1011の後に何が来るかはまだ学習していないので、この0の後の3つのジャンプは予測ミスする可能性がある。同じ種類の不規則性1回か2回の後では、分岐予測機構は0010の後に1、0101の後に1、1011の後に1が来ることを学習してしまう。その意味は、同じ種類の不規則性高々2回で、この種の不規則性を予測ミス1回だけで扱えるように学習してしまうということである。

二つの異なる規則的なパターンが交互に起きるときも、予測機構はたいへん有効である。例えば、000111というパターン(周期6)が何度も繰り返され、次にパターン01(周期2)が何度も、そして000111のパターンにもどるとすると、分岐予測機構は000111のパターンを再学習する必要はない。なぜなら、000111の列で使われるカウンタは、01の列ではいじらないからである。二つのパターンが2〜3回交替した後では、パターンの切り替え毎にたった1回だけの予測ミスで、パターンの変化も扱えるように学習してしまう。

12.2.6 完全には予測できないパターン(PMMX, PPro and PII)

完全には予測できない最も単純な分岐パターンは6回おきの分岐である。パターンは、
000001000001000001
    ^^    ^^    ^^
    ab    ab    ab
である。列0000の後には交互に、aの場所では0が、bの場所では1がくる。これはカウンタ0に影響し、毎回状態が上下する。もしカウンタ0がたまたま状態0で始まったら、状態0と1を交互に繰り返す。これは場所bで予測ミスすることになる。もしカウンタ0がたまたま3で始まったら、状態2と3を交互に繰り返し、場所aで予測ミスを起こす。最も悪い場合は状態2で始まったときである。カウンタ0は状態1と2を交互に繰り返し、場所aとbの両方で予測ミスが起きるという不運な成り行きとなる。(これは12.1.1節の終わりで説明したPPlainの最悪の場合と同類である)。この四つの状況のどれになるかは、この分岐へ割り当てる前の、BTBエントリの履歴による。ランダム割り当て法のため、これは制御できない。

原理的には、カウンタを望みの状態に持っていくように特別に設計された初期分岐列を与えることで、1周期で2回の予測ミスが起きる最悪の状況を避けることは可能である。しかしながら、そのようなアプローチは勧められない。なぜなら、コードをだいぶ余計に複雑にするひつようがあるし、カウンタに込めた情報は何であれ、タイマ割り込みやタスクスイッチの間に失われてしまいがちだからである。

12.2.7 完全にランダムなパターン(PMMX, PPro and PII)

パターン認識の強力な能力には、規則性の全然ない完全にランダムな列の場合には小さな欠点がある。 次の表は、ジャンプする/しないの完全にランダムな列についての予測ミスの、実験的に求めた割合を表している。
ジャンプする/しない 予測ミスの割合
---------------------------------------
0.001/0.999           0.001001
 0.01/0.99            0.0101
 0.05/0.95            0.0525
 0.10/0.90            0.110
 0.15/0.85            0.171
 0.20/0.80            0.235
 0.25/0.75            0.300
 0.30/0.70            0.362
 0.35/0.65            0.418
 0.40/0.60            0.462
 0.45/0.55            0.490
 0.50/0.50            0.500
---------------------------------------

プロセッサは、規則性の全然ない列の中で繰り返しパターンを見つけようとし続けるので、予測ミスの割合は、パターン認識なしでなるであろう割合より少し高い。

12.2.8 きついループ(PMMX)

パターン認識機構が次の分岐に出会う前にデータを更新する時間がないような小さいループでは、分岐予測は頼りにならない。この意味は、普通なら完全に予測できるような、単純なパターンを認識できないということである。偶然に、普通は認識できないようないくつかのパターンを、小さいループでは完全に予測する。例えば、毎回6回繰り返すループは、ループの末尾の分岐命令において、分岐パターン111110を持つ。このパターンでは普通は、繰り返し1回あたり1回または2回の予測ミスがあるが、きついループでは1回もない。同じことは7回繰り返すループにもあてはまる。他の繰り返し回数のループはほとんど、普通よりきついループのほうが予測がうまくいかない。この意味は、6回または7回繰り返すループはなるべくきつくするべきで、一方、他のループはなるべくきつくなくするべきだということである。ループをきつくなくする必要があるなら、ループを伸ばせばよい。

PMMXでループが「きつい」ふるまいをするかどうか知るためには、次のようなおおざっぱなやり方に従うとよい: ループ中の命令数を数えよ。もしそれが6以下なら、ループはきついふるまいをする。7命令より多ければ、パターン認識は普通に働くと、かなり確信してよい。不思議なことに、各命令が何クロックサイクルかかるか、ストールがあるか、ペアになるかどうかは関係ない。複雑な整数命令も変わりない。複雑な整数命令をたくさん含んでいて、きついふるまいをするループもあり得る。複雑な整数命令とは、ペアにできない整数命令でいつでも2クロックサイクル以上かかるもののことである。複雑な浮動小数点命令やMMX命令もまた1と数える。このおおざっぱな方法は発見的なもので、完全に信頼できるものではないことに気をつけてほしい。重要な場合には、自分でテストしたいかもしれない。PMMXでは、分岐予測ミスを数えるのに性能モニタカウンタの35H(PProとPIIでは0C5H)が使える。分岐予測は、割り当てに先立つBTBエントリの履歴に依存するかもしれないので、テストの結果は完全に決定的ではないかもしれない。

PProとPIIのきついループについては、正確な実験的情報を持っていない。

12.2.9 間接ジャンプとコール(PMMX, PPro and PII)

間接ジャンプやコールのためのパターン認識機構はなく、BTBは間接ジャンプのジャンプ先を一つしか覚えない。単純に、前回と同じジャンプ先にジャンプすると予測するだけである。

12.2.10 JECXZとLOOP(PMMX)

PMMXには、これら二つの命令のためのパターン認識機構はない。単純に、前回の実行と同じようになると予測するだけである。これら二つの命令は、PMMXの、時間的に決定的なコードでは、避けるべきである。

12.2.11 リターン(PMMX, PPro and PII)

PMMX、PPro、PIIプロセッサは Return Stack Buffer (RSB)を持っており、リターン命令を予測するのに使う。RSBは先入れ後出しバッファとして働く。CALL命令が実行される度に、対応する戻り番地がRSBに押し込まれる。そして、RET命令が実行される度に、戻り番地がRSBから引き出され、RETの予測のために使われる。この機構は、同じサブルーチンがいくつかの異なる場所から呼び出されるときにリターン命令が正しく予測されることを保証する。

この機構が確かに正しく働くようにするために、すべてのコールとリターンが対応しているようにしなければならない。速度が決定的なところでは、リターンを実行しないでサブルーチンから飛び出したり、リターンを間接ジャンプとして使ったりは決してしてはいけない。

PMMXでは、RSBは四つのエントリしか保持できない。RSBが空のときには、リターン命令は間接ジャンプと同じように、つまり、前回と同じジャンプ先に行くと予測される。サブルーチンが4段より深くネストするときは、最も内側の4段がRSBを使い、それより外側からのリターンでは、新しいコールがない限り、単純な予測機構を使う。RSBを使っているリターン命令もBTBエントリを専有する。

RSBの四つのエントリは多くないと思うかもしれないが、おそらく十分である。4段より深いサブルーチンのネスティングはたしかに珍しくはないが、速度については、最も深い部分だけが問題になる。

PProとPIIのRSBの大きさについての情報は持っていない。

12.2.12 静的予測(PMMX)

以前に出会ったことのない、すなわち、BTBに入っていない制御移行命令は、PMMXではいつでも通り抜けると予測される。前に行くか後ろに行くかは関係ない。

いつでも通り抜ける分岐命令はBTBエントリを持たない。いったん分岐すると直ちに、それはBTBに入り、何度通り抜けてもそこにとどまる。制御移行命令がBTBから出られるのは、他の制御移行命令にBTBエントリを盗られて押し出されたときだけである。

その直後の番地にジャンプするような制御移行命令は、BTBエントリを得ない。

例:
        JMP SHORT LL
LL:

この命令はBTBエントリを得ることは決してなく、そのためいつでも予測ミスのペナルティーがある。???

12.2.13 静的予測(PPro and PII)

PProとPIIでは、以前に出会ったことのない、すなわち、BTBに入っていない制御移行命令は、前に行く場合は通り抜けると予測され、後ろに行く(つまりループ)場合は分岐すると予測される。これらのプロセッサでは、静的予測は動的予測より長い時間がかかる。

12.2.14 近接したジャンプ(PMMX)

PMMXでは、二つの制御移行命令が互いに近過ぎると、同じBTBエントリを共有する恐れがある。その明白な結果は、いつでも予測ミスすることである。

制御移行命令のBTBエントリは、命令の最後のバイトの番地のビット2〜31で同定される。二つの制御移行命令が接近し過ぎて番地のビット0〜1しか違わないと、BTBエントリを共有する問題が起きる。

例:
        CALL    P
        JNC     SHORT L
もし、CALL命令の最後のバイトとJNC命令の最後のバイトがメモリの同じDWORDにはいっていると、ペナルティーがある。アセンブラの出力リストを見て、二つの番地がDWORD境界で分離されているかどうかを見なければならない。(DWORD境界とは、4で割り切れる番地のことである)。

この問題を解決するには、いろいろな方法がある。

  1. コード列をメモリ中で少し上か下に動かして、二つの番地の間にDWORD境界がくるようにする。
  2. shortジャンプをnearジャンプ(4バイトの変位)に変えて、命令の終わりがもっと下に行くようにする。命令の最短の形式以外を使うようにアセンブラに強制する方法はないので、この解決法を選んだときには、near分岐をハードコードする必要がある。
  3. CALL命令とJNC命令の間に何か命令を入れる。これは最も簡単な方法であり、セグメントがDWORDでアラインされていないとか、先行するコードを変更するにしたがってコードが上下するなどの理由で、DWORD境界がどこにあるかわからないときには、唯一の方法である。
            CALL    P
            MOV     EAX,EAX         ; 安全にするための、2バイトの詰め物
            JNC     SHORT L
    

もしPPlainでも問題を回避したいなら、代わりにNOPを二つ入れてペアリングを防ぐようにせよ(12.1.3節参照)。

RET命令はわずか1バイト長なので、この問題が特に起きやすい。

        JNZ     NEXT
        RET
ここでは最大3バイトの詰め物が必要である。
        JNZ     NEXT
        NOP
        MOV     EAX,EAX
        RET

12.2.15 連続するコールとリターン(PMMX)

コールの先のラベル続く最初の命令ペアが別のコール命令を含んでいたり、リターンが別のリターンの直後にあったりすると、ペナルティーがある。
FUNC1   PROC    NEAR
        NOP             ; コールの後のコールを避ける
        NOP
        CALL    FUNC2
        CALL    FUNC3
        NOP             ; リターンの後のリターンを避ける
        RET
FUNC1   ENDP
一つのNOPではCALLとペアになってしまうので、 CALL FUNC2 の前には二つのNOPが必要である。RETはペアになれないので、RETの前は一つのNOPで十分である。リターンの後のCALLにはペナルティーがないので、二つのCALL命令の間にNOPは必要ない。(PPlainではここにも二つのNOPが必要である)。

コールの連鎖のペナルティーは、同じサブルーチンが二つ以上の場所から呼ばれるときだけ起きる(おそらくRSBの更新が必要なため)。リターンの連鎖はいつでもペナルティーがある。コールの後のジャンプには時々小さなストールがあるが、コールの後のリターン、リターンの後のコール、ジャンプの後のジャンプとコールとリターン、リターンの後のジャンプには、ペナルティーはない。

12.2.16 分岐予測可能性のための設計(PMMX, PPro and PII)

多方向分岐(switch/case文)はジャンプ番地のリストを使った間接ジャンプか、分岐命令の木で実現される。間接ジャンプの予測は貧弱なので、簡単に予測できるパターンが期待できてBTBエントリが十分あるなら、後者の方法のほうが好ましい。

コードを再構成して、完全には予測できない分岐パターンを完全に予測できる別のパターンで置き換えたいと思うかもしれない。例えば、いつでも20回実行されるループを考えてみよう。ループの末尾の条件ジャンプは19回分岐し、20回目には毎回通り抜ける。このパターンは規則的であるが、パターン認識機構では認識できない。これを4回と5回のネストしたループにするか、ループを4回伸ばして5回実行するかして、認識できるパターンだけにすることができる。この種の複雑なスキームは、PProとPIIのような予測ミスが非常に高価なプロセッサでだけ余計なコストに見合う価値がある。これより大きいループ回数では、たった一つの予測ミスについて何かする理由は何もない。

12.3 分岐を避ける

時には、分岐と同じ効果をビットとフラグの巧妙な操作で得られる。例えば、符号つき数の絶対値を分岐なしで計算できる。
        CWD
        XOR EAX,EDX
        SUB EAX,EDX
(PPlainとPMMXでは、CWDの代わりに MOV EDX,EAX / SAR EDX,31 を使う)。

キャリーフラグはこの種のトリックには特に役に立つ。

値が0ならキャリーを立てる:  CMP [value],1
値が0でなければキャリーを立てる:  XOR EAX,EAX / CMP EAX,[value]
キャリーならカウンタを増やす:  ADC EAX,0
キャリーが立つたびにビットをセットする:  RCL EAX,1
キャリーが立っているならビットマスクを生成する:  SBB EAX,EAX
任意の条件でビットをセットする:  SETcond AL
任意の条件で8ビットをセットする:  SETNcond AL / DEC AL
(最後の例では、条件を反転するのを忘れないように)

この例は、二つの符号なし数の小さいほうを見つける: if (b < a) a = b;

        SUB EBX,EAX
        SBB ECX,ECX
        AND ECX,EBX
        ADD EAX,ECX

この例は二つの数の一つを選ぶ: if (a != 0) a = b; else a = c;

        CMP EAX,1
        SBB EAX,EAX
        XOR ECX,EBX
        AND EAX,ECX
        XOR EAX,EBX

このようなトリックが余分なコードに見合うかどうかは、条件ジャンプがどれだけ予測できるか、分岐のないコードで増えたペアリングの機会を利用できるかどうか、そして、連続するジャンプのペナルティーを受けるようなジャンプが直後にあるかどうかによる。

PProとPIIプロセッサは、特に分岐を避けることを意図した、条件つきMOV命令を持っている。これらのプロセッサでだけ走るようなコードでは、予測のうまくいかない分岐は可能ならすべて条件つきMOVで置き換えるべきである。


13. プリフィックス(PPlain and PMMX)

一つまたは複数のプリフィックスを持つ命令は、Vパイプで実行できないかもしれず(段落8.7参照)、デコードに2クロック以上かかるかもしれない。

PPlainでは、near条件ジャンプの0Fhプリフィックスを除いて、デコードの遅れは各プリフィックスあたり1クロックサイクルである。

PMMXでは、0Fhプリフィックスについてのデコードの遅れはない。セグメントとリピートプリフィックスはデコードに1クロック余計にかかる。アドレスとオペランドサイズプリフィックスはデコードに2クロック余計にかかる。最初の命令がセグメントかリピートプリフィックスを持っているか、プリフィックスを持たず、二番目の命令がプリフィックスを持たないなら、PMMXはクロックサイクルあたり2命令デコードできる。アドレスまたはオペランドプリフィックスを持つ命令は、PMMXでは単独でしかデコードできない。二つ以上のプリフィックスを持つ命令は各プリフィックスについて1クロック余計にかかる。

アドレスサイズプリフィックスは32ビットモードを使うことで避けられる。セグメントプリフィックスは、32ビットモードでは、フラットメモリモデルを使うことで避けられる。オペランドサイズプリフィックスは、32ビットモードでは、8ビットと32ビットの整数だけを使うことで避けられる。

プリフィックスが避けられない場所では、先行する命令が実行に2クロック以上かかるなら、デコードの遅れはマスクされるかもしれない。PPlainのための規則は次の通りである。実行(デコードではない)にNクロックサイクルかかる任意の命令は、次の二つ(ときには三つ)の命令または命令ペアのN-1個のプリフィックスのデコードの遅れに「影を落とす」ことができる。言い換えれば、命令の実行にかかる余分なクロックは、それぞれ後の命令のプリフィックス一つをデコードするのに使えるということである。この影落とし効果は予測できた分岐をも越えて拡張される。2クロックサイクル以上かかる命令、AGIストール、キャッシュミス、ミスアラインメント、そのほか、デコードの遅れや分岐予測ミスを除くどんな理由によってでも遅れる命令は何でも、影落とし効果を持つ。

PMMXは、同様の影落とし効果をもつが、その機構は異なる。デコードされた命令は透過な first-in-first-out (FIFO) バッファに格納され、バッファは4つまでの命令を保持できる。FIFOバッファに命令がある限り、遅れはない。バッファが空のときは、命令はデコードされるとすぐに実行される。命令が実行されるよりデコードされるのが速いとき、つまり、ペアにならない、または複数サイクルの命令があるときに、バッファは満たされる。命令がデコードされるより実行されるのが速いとき、つまり、プリフィックスによるデコードの遅れがあるとき、FIFOバッファは空になる。予測ミスした分岐の後は、FIFOバッファは空である。二番目の命令はプリフィックスなしで、どちらの命令も7バイトより長くないという前提で、FIFOバッファはクロックサイクルあたり2命令を受け取れる。二つの実行パイプライン(UとV)は、クロックサイクルあたりそれぞれFIFOバッファから1命令を受け取れる。

例:
CLD / REP MOVSD
CLD命令は2クロックサイクルかかり、従ってREPプリフィックスのデコードの遅れに影を落とす。もしCLD命令が REP MOVSD から遠くにあったとしたら、コードはもう1クロックサイクルかかっていただろう。
CMP DWORD PTR [EBX],0 / MOV EAX,0 / SETNZ AL
CMP命令はここではread/modify命令なので、2クロックサイクルかかる。SETNZ命令の0FhプリフィックスはCMP命令の第2クロックサイクルの間にデコードされるので、PPlainではデコードの遅れは隠される(PMMXは0FHのデコードの遅れはない)。

14. コードサイズの縮小

6章で説明したように、コードキャッシュは8KBまたは16KBである。コードの決定的に重要な部分をコードキャッシュに納めるのに問題があるのなら、コードのサイズを縮小することを考慮するのもよい。

アドレスとデータの定数は、32ビットコードでは4バイトかかるが16ビットコードでは2バイトしかかからないので、普通は、32ビットコードは16ビットコードより大きい。しかしながら、16ビットコードには、プリフィックスや、隣り合うワードを同時にアクセスするときの問題(10章参照)のような他のペナルティーがある。コードのサイズを減らす別の方法を以下で議論する。

ジャンプの番地、データの番地、データ定数はどれも、符号拡張されるバイトで表現できるなら、つまり、-128から+127までの範囲なら、少ないスペースですむ。

ジャンプの番地については、これの意味は、短いジャンプはコードが2バイトしかかからないが、一方、127バイトを越えるジャンプは無条件ジャンプなら5バイト、条件ジャンプなら6バイトかかるということである。

同様に、データの番地がポインタと-128から+127までの変位で表現できるなら、少ないスペースですむ。

例:
MOV EBX,DS:[100000] / ADD EBX,DS:[100004]          ; 12バイト
これを縮小して:
MOV EAX,100000 / MOV EBX,[EAX] / ADD EBX,[EAX+4]   ; 10バイト
ポインタを使うのは、それを何度も使うほど有利になる。だから、データをスタックに格納し、EBPまたはESPをポインタとして使うことは、もちろんデータがポインタから127バイト以内であるという前提で、静的メモリと絶対番地を使うことに比べて、コードを小さくする。一時データの読み書きにPUSHとPOPを使うことは、さらにもっと有利である。

データ定数も-128から+127の間にあれば少ないスペースですむ。即値をもつ多くの命令は、オペランドが符号拡張されるバイトであるような短い形式を持つ。

例:
PUSH 200      ; 5バイト
PUSH 100      ; 2バイト

ADD EBX,128   ; 6バイト
SUB EBX,-128  ; 3バイト

短い形式のない即値つき命令で最も重要なのは、MOVである。

例:
MOV EAX, 1              ; 5バイト
これは次のように変えるとよい:
XOR EAX,EAX / INC EAX   ; 3バイト
または
PUSH 1 / POP EAX        ; 3バイト

4バイトの即値オペランドを持つMOVは、MOVの前のレジスタの値がわかっていれば、算術演算命令で置き換えるとよいことがある。

例:
        MOV     [mem1],0        ; 10バイト
        MOV     [mem2],-1       ; 10バイト
        MOV     EAX,100         ;  5バイト
        MOV     EBX,150         ;  5バイト
これは次のように変更するとよい:
        XOR     EAX,EAX         ;  2バイト
        MOV     [mem1],EAX      ;  5バイト
        DEC     EAX             ;  1バイト
        MOV     [mem2],EAX      ;  5バイト
        ADD     EAX,101         ;  3バイト
        LEA     EBX,[EAX+50]    ;  3バイト
LEA命令のAGIストールに気をつけてほしい。

もし同じ定数を2回以上使うなら、もちろんレジスタにロードするのがよい。

例:
MOV DWORD PTR [EBX],0 / MOV DWORD PTR [EBX+4],0   ; 13バイト
XOR EAX,EAX / MOV [EBX],EAX / MOV [EBX+4],EAX     ;  7バイト

異なる命令は異なる長さを持つことも考慮してよい。次の命令は1バイトしかかからず、従ってたいへん魅力的である。

PUSH reg, POP reg, INC reg32, DEC reg32
8ビットレジスタのINCとDECは2バイトかかるので、 INC EAX は INC AL より短い。

XCHG EAX,reg も1バイトの命令で、従って MOV EAX,reg より少ないスペースですむが、遅いし、ペアにできない。

いくつかの命令は、アキュームレータを使ったほうが、他のレジスタを使うより1バイト少なくてすむ。

例:
MOV EAX,DS:[100000]  は  MOV EBX,DS:[100000]  より短い
ADD EAX,1000         は  ADD EBX,1000         より短い
ポインタを使う命令は、(ESPでない)ベースポインタと変位だけのときは、スケールつきインデックスレジスタを持つときや、ベースポインタとインデックスレジスタを持つときや、ベースポインタとしてESPを持つときと比べて、1バイト短くてすむ。
例:
MOV EAX,[array][EBX]  は  MOV EAX,[array][EBX*4]  より小さい
MOV EAX,[EBP+12]      は  MOV EAX,[ESP+12]        より小さい
EBPをベースポインタとして持ち、変位やインデックスがない命令は、他のレジスタと比べて1バイト多くかかる(訳注: インデックスつきの場合も、他のレジスタと比べて1バイト多くかかる)。
MOV EAX,[EBX]    は  MOV EAX,[EBP]    より小さいが
MOV EAX,[EBX+4]  は  MOV EAX,[EBP+4]  と同じサイズ

15. 浮動小数点コードのスケジューリング(PPlain and PMMX)

浮動小数点命令は、次の規則で定義される一つの特別な場合を除いて、整数命令と同じようにペアになることはできない: この特別なペアリングは、後に簡単に説明するように、重要である。

たいがいの浮動小数点命令はペアにできないが、多くはパイプラインにできる。つまり、前の命令が終わる前に命令を開始できる。

例:
FADD ST(1),ST(0)   ; クロックサイクル1-3
FADD ST(2),ST(0)   ; クロックサイクル2-4
FADD ST(3),ST(0)   ; クロックサイクル3-5
FADD ST(4),ST(0)   ; クロックサイクル4-6

明らかに、二番目の命令が最初の命令の結果を必要としていたら、二つの命令がオーバーラップできない。ほとんどすべての浮動小数点命令は、スタックレジスタのトップであるST(0)に関わるので、命令を前の命令の結果に依存しないようにできる可能性はあまり多くないように見える。この問題の解決策は、レジスタリネーミングである。FXCH命令は、本当は二つのレジスタの内容を交換するのではない。名前を交換するだけである。レジスタスタックをpushまたはpopする命令もリネーミングによって動作する。Pentiumでは、浮動小数点レジスタリネーミングはたいそう最適化されており、レジスタの使用中でもリネーム可能である。レジスタリネーミングは決してストールを起こさない―同じくロックサイクルでレジスタを2回以上リネームすることさえできる。例えば、FLDまたはFCOMPPをFXCHとペアにしたときである。

FXCH命令の適切な使用によって、浮動小数点コードで多くのオーバーラップを達成できる。

例:
FLD     [a1]    ; クロックサイクル1
FADD    [a2]    ; クロックサイクル2-4
FLD     [b1]    ; クロックサイクル3
FADD    [b2]    ; クロックサイクル4-6
FLD     [c1]    ; クロックサイクル5
FADD    [c2]    ; クロックサイクル6-8
FXCH    ST(2)   ; クロックサイクル6
FADD    [a3]    ; クロックサイクル7-9
FXCH    ST(1)   ; クロックサイクル7
FADD    [b3]    ; クロックサイクル8-10
FXCH    ST(2)   ; クロックサイクル8
FADD    [c3]    ; クロックサイクル9-11
FXCH    ST(1)   ; クロックサイクル9
FADD    [a4]    ; クロックサイクル10-12
FXCH    ST(2)   ; クロックサイクル10
FADD    [b4]    ; クロックサイクル11-13
FXCH    ST(1)   ; クロックサイクル11
FADD    [c4]    ; クロックサイクル12-14
FXCH    ST(2)   ; クロックサイクル12
上の例では、三つの独立なスレッドをインターリーブしている。各FADDは3クロックサイクルかかり、新しいFADDを各クロックサイクルで開始できる。FADDを'a'スレッドで開始したら、'a'スレッドに戻る前に、二つの新しいFADD命令を'b'と'c'のスレッドで開始する時間がある。このため、3つ毎のFADD命令が同じスレッドに属する。望みのスレッドに属するレジスタをST(0)に持ってくるのに、FXCH命令を毎回使っている。上の例からわかるように、これは規則的なパターンを生成するが、FXCH命令の繰り返しの周期が2あるのに対して、スレッドの周期は3であることに注意してほしい。これはたいそう混乱を招くので、どのレジスタがどこにあるか知るためには、「コンピュータを再生」しなければならない。

FADD,FSUB,FMUL,FILDのすべてのバージョンは3クロックサイクルかかり、オーバーラップ可能である。そのため、これらの命令は上に示した方法でスケジューリングできる。メモリオペランドがレベル1キャッシュにあって適切にアラインされていれば、メモリオペランドはレジスタオペランドより時間がかかることはない。

これまでにあなたは、例外のある規則に慣れてきたに違いない。そして、オーバーラップの規則も例外ではない。FMUL命令を別のFMUL命令の1クロックサイクル後に始めることはできない。FMULの回路が完全にはパイプライン化されていないからである。別の命令を二つのFMULの間に入れることを勧める。

例:
FLD     [a1]    ; クロックサイクル1
FLD     [b1]    ; クロックサイクル2
FLD     [c1]    ; クロックサイクル3
FXCH    ST(2)   ; クロックサイクル3
FMUL    [a2]    ; クロックサイクル4-6
FXCH            ; クロックサイクル4
FMUL    [b2]    ; クロックサイクル5-7    (ストール)
FXCH    ST(2)   ; クロックサイクル5
FMUL    [c2]    ; クロックサイクル7-9    (ストール)
FXCH            ; クロックサイクル7
FSTP    [a3]    ; クロックサイクル8-9
FXCH            ; クロックサイクル10     (ペアにならない)
FSTP    [b3]    ; クロックサイクル11-12
FSTP    [c3]    ; クロックサイクル13-14
ここで、 FMUL [b2] の前と FMUL [c2] の前では、先立つクロックサイクルで別のFMULが始まっているので、ストールを受ける。このコードは、FMULの間にFLD命令を置くことで改良できる:
FLD     [a1]    ; クロックサイクル1
FMUL    [a2]    ; クロックサイクル2-4
FLD     [b1]    ; クロックサイクル3
FMUL    [b2]    ; クロックサイクル4-6
FLD     [c1]    ; クロックサイクル5
FMUL    [c2]    ; クロックサイクル6-8
FXCH    ST(2)   ; クロックサイクル6
FSTP    [a3]    ; クロックサイクル7-8
FSTP    [b3]    ; クロックサイクル9-10
FSTP    [c3]    ; クロックサイクル11-12
他の場合には、FADD,FSUBまたは他の何でもいいからFMULの間に入れてストールを避ければよい。

浮動小数点命令をオーバーラップさせることはもちろん、インターリーブ可能な独立したスレッドがいくつかあることを必要とする。もし、一つの大きな式しか実行するものがないなら、式の各部分を並列に計算して、オーバーラップを実現してもよい。もし、例えば、六つの数を足したいのなら、演算を三つの数から成る二つのスレッドに分けて、二つのスレッドを最後に足せばよい。

FLD     [a]     ; クロックサイクル1
FADD    [b]     ; クロックサイクル2-4
FLD     [c]     ; クロックサイクル3
FADD    [d]     ; クロックサイクル4-6
FXCH            ; クロックサイクル4
FADD    [e]     ; クロックサイクル5-7
FXCH            ; クロックサイクル5
FADD    [f]     ; クロックサイクル7-9    (ストール)
FADD            ; クロックサイクル10-12  (ストール)
FADD [f] は FADD [d] の結果を待っているので、その前で1クロックのストールを受ける。また、最後のFADDは FADD [f] の結果を待っているので、2クロックのストールを受ける。後者のストールは整数命令をいくつか詰めることで隠せるが、最初のストールは、ここに整数命令を入れるとFXCHがペアにできなくなるので、隠せない。

最初のストールは、二つではなく三つのスレッドを持つことで避けられるが、余計なFLDを要するので、少なくとも足す数が八つないと、スレッドを二つから三つにすることで何も節約できない。

浮動小数点命令のすべてがオーバーラップできるわけではない。いくつかの浮動小数点命令は、引き続く浮動小数点命令よりも整数命令のほうが多くオーバーラップできる。FDIV命令は、例えば、39クロックサイクルかかる。最初を除くすべてのクロックサイクルは整数命令とオーバーラップできるが、浮動小数点命令とは最後の2クロックしかオーバーラップできない。

例:
FDIV            ; クロックサイクル1-39  (Uパイプ)
FXCH            ; クロックサイクル1-2   (Vパイプ、不完全なペア)
SHR EAX,1       ; クロックサイクル3     (Uパイプ)
INC EBX         ; クロックサイクル3     (Vパイプ)
CMC             ; クロックサイクル3-4   (ペアにできない)
FADD [x]        ; クロックサイクル38-40 (Uパイプ、f.p.ユニット解放を待つ)
FXCH            ; クロックサイクル38    (Vパイプ)
FMUL [y]        ; クロックサイクル40-42 (Uパイプ、FDIVの結果を待つ)
最初のFXCH命令はFDIVとペアになるが、浮動小数点命令が続かないため余計なクロックがかかる。 SHR / INC ペアはFDIVが終わる前に始まるが、FXCH命令が終わるまで待たなければならない。新しい浮動小数点命令はFDIVの最後の2クロックサイクルでしか実行できないので、FADDはクロック38まで待たなければならない。2番目のFXCHはFADDとペアになる。FMULは割り算の結果を使うので、FDIVが終わるのを待たなければならない。

整数とのオーバーラップの長い浮動小数点命令の後に入れるものが他になければ、プログラムの後のほうで必要だと期待される番地からの、ダミーの読み出しを置いて、必ずそれがレベル1キャッシュに入るようにしてもよい。

例:
        FDIV    QWORD PTR [EBX]
        CMP     [ESI],EAX
        FMUL    QWORD PTR [ESI]
ここでは整数のオーバーラップを、FDIV命令の計算中に、[ESI]にある値をあらかじめキャッシュに入れておくために使っている(CMPの結果は気にしない)。

21章に浮動小数点命令の完全なリストと、何とペアになったりオーバーラップできたりするかが示してある。

算術演算ユニットはパイプライン中で読み込みユニットの一つ後のステップにあるので、浮動小数点命令にメモリオペランドを使うことに対するペナルティーはない。これのトレードオフは浮動小数点データをメモリに格納するときに現れる。メモリへのFSTまたはFSTP命令は実行ステージで2クロックサイクルかかるが、1クロック前にデータを必要とするので、格納する値が1クロック前に準備できていないと1クロックのストールを受ける。これはAGIストールと同様である。

例:
FLD     [a1]    ; クロックサイクル1
FADD    [a2]    ; クロックサイクル2-4
FLD     [b1]    ; クロックサイクル3
FADD    [b2]    ; クロックサイクル4-6
FXCH            ; クロックサイクル4
FSTP    [a3]    ; クロックサイクル6-7
FSTP    [b3]    ; クロックサイクル8-9
FSTP [a3] は、 FADD [a2] の結果が先立つクロックサイクルで準備できていないので、ストールする。多くの場合、浮動小数点コードを四つのスレッドでスケジュールするか、整数命令をいくつか間にはさむことなく、この型のストールを隠すことはできない。FST(P)命令の実行ステージの2クロックは、引き続く命令とペアにしたりオーバーラップしたりすることはできない。

FIADD,FISUB,FIMUL,FIDIV,FICOMのような整数オペランドを持つ命令は、オーバーラップを改良するためにより簡単な命令に分割するとよい。

例:
FILD    [a]     ; クロックサイクル1-3
FIMUL   [b]     ; クロックサイクル4-9
を分割して:
FILD    [a]     ; クロックサイクル1-3
FILD    [b]     ; クロックサイクル2-4
FMUL            ; クロックサイクル5-7
この例では、二つのFILD命令をオーバーラップさせることで2クロックを節約する。

【訳注】これは誤りで、実際には分割しなくても、FIMULはFILDと2クロックオーバーラップして実行される。


16. ループの最適化(PPlain and PMMX)

プログラムを分析すると、ほとんどの時間消費が最も内側のループにあることにしばしば気がつくだろう。速度を改良する方法は、最も時間を消費するループを、アセンブリ言語を使って注意深く最適化することである。プログラムの残りの部分は高級言語で残しておいてもよい。

ループはたいてい、何回繰り返すかを制御するカウンタを持ち、しばしば、各繰り返しで一つの要素を読むか書くかする配列アクセスを含む。私は例として、配列から整数を読み、各整数の符号を変更し、結果を別の配列に格納する手続きを選んだ。

この手続きのC言語のコードは次のようになるだろう:

void ChangeSign (int * A, int * B, int N) {
  int i;
  for (i=0; i<N; i++) B[i] = -A[i];}
アセンブラに翻訳すると、このような手続きを書くだろう:
例1:

_ChangeSign PROCEDURE NEAR
        PUSH    ESI
        PUSH    EDI
A       EQU     DWORD PTR [ESP+12]
B       EQU     DWORD PTR [ESP+16]
N       EQU     DWORD PTR [ESP+20]

        MOV     ECX, [N]
        JECXZ   L2
        MOV     ESI, [A]
        MOV     EDI, [B]
        CLD
L1:     LODSD
        NEG     EAX
        STOSD
        LOOP    L1
L2:     POP     EDI
        POP     ESI
        RET                     ; (Cの呼出規則では余分なpopは不要)
_ChangeSign     ENDP
これはうまい答えのように見えるが、遅くてペアにできない命令を含んでいるので、最適ではない。すべてのデータがレベル1キャッシュにある場合、繰り返し当たり11クロックサイクルかかる。

ペアにできる命令だけを使う(PPlain and PMMX)

例2:

        MOV     ECX, [N]
        MOV     ESI, [A]
        TEST    ECX, ECX
        JZ      SHORT L2
        MOV     EDI, [B]
L1:     MOV     EAX, [ESI]       ; u
        XOR     EBX, EBX         ; v (ペアになる)
        ADD     ESI, 4           ; u
        SUB     EBX, EAX         ; v (ペアになる)
        MOV     [EDI], EBX       ; u
        ADD     EDI, 4           ; v (ペアになる)
        DEC     ECX              ; u
        JNZ     L1               ; v (ペアになる)
L2:
ここではペアにできる命令だけを使い、すべてペアになるように命令をスケジュールした。今や繰り返し当たり4クロックしかかからない。NEG命令を分割することなく同じ速度を得ることはできただろうが、他のペアにできない命令は分割が必要である。

カウンタと添字に同じレジスタを使う

例3:

        MOV     ESI, [A]
        MOV     EDI, [B]
        MOV     ECX, [N]
        XOR     EDX, EDX
        TEST    ECX, ECX
        JZ      SHORT L2
L1:     MOV     EAX, [ESI+4*EDX]          ; u
        NEG     EAX                       ; u
        MOV     [EDI+4*EDX], EAX          ; u
        INC     EDX                       ; v (ペアになる)
        CMP     EDX, ECX                  ; u
        JB      L1                        ; v (ペアになる)
L2:
カウンタと添字に同じレジスタを使うことで、ループ本体の命令が少なくなるが、ペアにできない命令が二つあるので、まだ4クロックサイクルかかる。

カウンタを0で終わりにする

例2と同じように、カウンタを0で終わりにして、終了をゼロフラグで判定するようにすることで、CMP命令を取り除きたい。これを行う一つの方法は、ループを逆に実行して、配列の最後の要素を最初に処理することである。しかしながら、データキャッシュはデータを逆順ではなく正順にアクセスするために最適化されているので、キャッシュミスがありがちならむしろ、カウンタを-Nから始めて負の値を0になるまで数えるべきである。それならベースレジスタは配列の最初ではなく最後を指すべきである。
例4:

        MOV     ESI, [A]
        MOV     EAX, [N]
        MOV     EDI, [B]
        XOR     ECX, ECX
        LEA     ESI, [ESI+4*EAX]          ; 配列Aの終わりを指す
        SUB     ECX, EAX                  ; -N
        LEA     EDI, [EDI+4*EAX]          ; 配列Bの終わりを指す
        JZ      SHORT L2
L1:     MOV     EAX, [ESI+4*ECX]          ; u
        NEG     EAX                       ; u
        MOV     [EDI+4*ECX], EAX          ; u
        INC     ECX                       ; v (ペアになる)
        JNZ     L1                        ; u
L2:
今やループ本体は5命令まで減ったが、ペアリングがうまくいっていないので、まだ4クロックかかる。(もし配列の番地と大きさが定数なら、ESIの代わりにA+SIZE A、EDIの代わりにB+SIZE Bを使うことでレジスタを二つ節約できる)。今度は、ペアリングがどれくらい改良できるか見てみよう。

計算をループのオーバーヘッドとペアにする(PPlain and PMMX)

計算をループ制御命令と混ぜ合わせることでペアリングを改良したい。もし、INC ECX と JNZ L1 の間に何か入れたいなら、ゼロフラグに影響のないものでなければならない。 INC ECX の後の MOV [ESI+4*ECX],EBX 命令はAGIを起こすだろうから、もっと賢くやらなければならない:
例5:

        MOV     EAX, [N]
        XOR     ECX, ECX
        SHL     EAX, 2                    ; 4 * N
        JZ      SHORT L3
        MOV     ESI, [A]
        MOV     EDI, [B]
        SUB     ECX, EAX                  ; - 4 * N
        ADD     ESI, EAX                  ; 配列Aの終わりを指す
        ADD     EDI, EAX                  ; 配列Bの終わりを指す
        JMP     SHORT L2
L1:     MOV     [EDI+ECX-4], EAX          ; u
L2:     MOV     EAX, [ESI+ECX]            ; v (ペアになる)
        XOR     EAX, -1                   ; u
        ADD     ECX, 4                    ; v (ペアになる)
        INC     EAX                       ; u
        JNC     L1                        ; v (ペアになる)
        MOV     [EDI+ECX-4], EAX
L3:
ここではEAXの符号反転を計算するのに異なる方法を使った。私がこの方法を使っている理由は、INC命令のきたないトリックを使えるからである。ADDはキャリーフラグを変えるが、INCは変えない。ループカウンタを増やすのには、INCの代わりにADDを使い、ゼロフラグの代わりにキャリーフラグをテストしている。これでキャリーフラグに影響なく INC EAX をはさむことが可能になる。 INC EAX の代わりに LEA EAX,[EAX+1] を使ってもよかったと思うかもしれない。それは少なくともどのフラグも変えないが、LEA命令はAGIを起こすだろうから最適解ではない。

ここでは完全なペアリングが達成できており、ループは今や3クロックサイクルしかかからない。ループカウンタを1増やす(例4のように)か、4増やす(例5のように)かは趣味の問題である。ループのタイミングに違いはない。

一つの操作の最後を次の操作の始めとオーバーラップさせる

例5で使った方法はあまり一般的に使える方法ではないので、ペアリングの機会を改良するの他の方法を探そう。一つの方法は、ループを再構成して一つの操作の最後を次の操作の始めとオーバーラップさせることである。これを私はループ巻き込みと呼びたい。巻き込まれたループは、ループの各繰返しの最後の操作が未完になっていて、それは次の繰返しで完了する。実際、一つの繰返しの最後のMOVと次の繰返しの最初のMOVがペアになるが、この方法をさらに探求したい。
例6:

        MOV     ESI, [A]
        MOV     EAX, [N]
        MOV     EDI, [B]
        XOR     ECX, ECX
        LEA     ESI, [ESI+4*EAX]          ; 配列Aの終わりを指す
        SUB     ECX, EAX                  ; -N
        LEA     EDI, [EDI+4*EAX]          ; 配列Bの終わりを指す
        JZ      SHORT L3
        XOR     EBX, EBX
        MOV     EAX, [ESI+4*ECX]
        INC     ECX
        JZ      SHORT L2
L1:     SUB     EBX, EAX                  ; u
        MOV     EAX, [ESI+4*ECX]          ; v (ペアになる)
        MOV     [EDI+4*ECX-4], EBX        ; u
        INC     ECX                       ; v (ペアになる)
        MOV     EBX, 0                    ; u
        JNZ     L1                        ; v (ペアになる)
L2:     SUB     EBX, EAX
        MOV     [EDI+4*ECX-4], EBX
L3:
ここでは、最初の値を格納する前に2番目の値を読み始め、これはもちろんペアリングの機会を改良する。ペアリングを改良するためではなく、AGIを避けるために、 MOV EBX,0 命令を INC ECX と JNZ L1 の間に置く。

ループを伸ばす

ペアリングの機会を改良する最も一般的に使える方法は、各実行で2回の操作を行い、実行回数を半分にすることである。これを、ループを伸ばす(rolling out a loop)という:
例7:

        MOV     ESI, [A]
        MOV     EAX, [N]
        MOV     EDI, [B]
        XOR     ECX, ECX
        LEA     ESI, [ESI+4*EAX]          ; 配列Aの終わりを指す
        SUB     ECX, EAX                  ; -N
        LEA     EDI, [EDI+4*EAX]          ; 配列Bの終わりを指す
        JZ      SHORT L2
        TEST    AL,1                      ; Nが奇数かどうか調べる
        JZ      SHORT L1
        MOV     EAX, [ESI+4*ECX]          ; Nが奇数なら、その半端を処理する
        NEG     EAX
        MOV     [EDI+4*ECX], EAX
        INC     ECX                       ; カウンタを偶数にする
        JZ      SHORT L2                  ; N = 1
L1:     MOV     EAX, [ESI+4*ECX]          ; u
        MOV     EBX, [ESI+4*ECX+4]        ; v (ペアになる)
        NEG     EAX                       ; u
        NEG     EBX                       ; u
        MOV     [EDI+4*ECX], EAX          ; u
        MOV     [EDI+4*ECX+4], EBX        ; v (ペアになる)
        ADD     ECX, 2                    ; u
        JNZ     L1                        ; v (ペアになる)
L2:
今度は二つの操作を並列に行い、これは最高のペアリング機会を提供する。ループは偶数回の操作しかできないので、Nが奇数かどうかテストして、もしそうならループの外で1回分の操作をしなければならない。

このループは最初のMOV命令でAGIがある。先立つクロックサイクルでECXが増やされるからである。それでループは2回分の操作に6クロックサイクルかかる。

AGIを除くためにループを再構成する(PPlain and PMMX)

例8:

        MOV     ESI, [A]
        MOV     EAX, [N]
        MOV     EDI, [B]
        XOR     ECX, ECX
        LEA     ESI, [ESI+4*EAX]          ; 配列Aの終わりを指す
        SUB     ECX, EAX                  ; -N
        LEA     EDI, [EDI+4*EAX]          ; 配列Aの終わりを指す
        JZ      SHORT L3
        TEST    AL,1                      ; Nが奇数かどうか調べる
        JZ      SHORT L2
        MOV     EAX, [ESI+4*ECX]          ; Nが奇数なら、その半端を処理する
        NEG     EAX                       ; ペアリングの機会はない
        MOV     [EDI+4*ECX-4], EAX
        INC     ECX                       ; カウンタを偶数にする
        JNZ     SHORT L2
        NOP                               ; JNZ L2 が予測できないなら、NOPを追加
        NOP
        JMP     SHORT L3                  ; N = 1
L1:     NEG     EAX                       ; u
        NEG     EBX                       ; u
        MOV     [EDI+4*ECX-8], EAX        ; u
        MOV     [EDI+4*ECX-4], EBX        ; v (ペアになる)
L2:     MOV     EAX, [ESI+4*ECX]          ; u
        MOV     EBX, [ESI+4*ECX+4]        ; v (ペアになる)
        ADD     ECX, 2                    ; u
        JNZ     L1                        ; v (ペアになる)
        NEG     EAX
        NEG     EBX
        MOV     [EDI+4*ECX-8], EAX
        MOV     [EDI+4*ECX-4], EBX
L3:
トリックは、ループカウンタを添字として使わない命令ペアを見つけ、ループを再構成して、前のクロックサイクルでカウンタが増えるようにすることである。今や二つの操作で5クロックサイクルまで減り、可能な最高に近くなった。 データのキャッシングが決定的に重要なら、AとBの配列を結合して、一つの構造を持った配列にし、各B[i]が対応するA[i]の直後に来るようにすることで、速度をさらに改良できるだろう。構造を持った配列が少なくとも8でアラインされていれば、B[i]はいつもA[i]と同じキャッシュラインに入るので、B[i]に書くときにキャッシュミスは決してない。これはもちろんプログラムの他の部分とトレードオフがあるので、利益に対してコストを比べなければならない。

2回を超えて伸ばす

操作当たりのループのオーバーヘッドを減らすために、繰り返し当たり2回より多くの操作を行うことを考えるかもしれない。しかし、ほとんどの場合のループオーバーヘッドは、繰り返し当たりたったの1クロックサイクルに減らせるので、ループを2回でなく4回伸ばしても、操作当たり1/4クロックサイクルしか節約できず、その労力に値しないだろう。ループのオーバーヘッドが1クロックサイクルに減らせず、Nが非常に大きいときに限って、4回伸ばすことを考えるべきである。

ループを過剰に伸ばすことの欠点は次の通りである:

  1. Rを伸ばす回数とすると、 N MODULO R を計算して、 N MODULO R 回の操作をメインループの前か後で行い、残りの操作の数をRで割り切れるようにする必要がある。これは余分なコードと予測しにくい分岐を要する。ループ本体ももちろん大きくなる。
  2. コード片は普通、初めて実行されるときに非常に多くの時間がかかり、初回実行のペナルティーはコードが多くなればなるほど、特にNが小さいとき、大きくなる。
  3. 過剰なコードサイズはコードキャッシュの利用率を下げる。

32ビットレジスタで、複数の8または16ビットオペランドを同時に扱う

8または16ビットオペランドの配列を操作する必要があるなら、メモリアクセスの操作を二つペアにすることができないかもしれないので、伸ばしたループには問題がある。例えば、 MOV AL,[ESI] / MOV BL,[ESI+1] は、二つのオペランドがメモリの同じDWORD内にあるなら、ペアにならないのである。しかしもっと賢い方法があるかもしれない。つまり、同じ32ビットレジスタで4バイトを一度に扱うことである。

次の例はバイトの配列のすべての要素に2を加える。

例9:

        MOV     ESI, [A]         ; バイト配列の番地
        MOV     ECX, [N]         ; バイト配列中の要素数
        TEST    ECX, ECX         ; Nが0かどうかテスト
        JZ      SHORT L2
        MOV     EAX, [ESI]       ; 最初の4バイトを読む
L1:     MOV     EBX, EAX         ; EBXにコピー
        AND     EAX, 7F7F7F7FH   ; EAXの各バイトの下位7ビット
        XOR     EBX, EAX         ; EBXの各バイトの最上位ビットを得る
        ADD     EAX, 02020202H   ; 望みの値を4バイトすべてに加える
        XOR     EBX, EAX         ; 再びビットを組み合わせる
        MOV     EAX, [ESI+4]     ; 次の4バイトを読む
        MOV     [ESI], EBX       ; 結果を格納する
        ADD     ESI, 4           ; ポインタを増やす
        SUB     ECX, 4           ; ループカウンタを減らす
        JA      L1               ; ループ
L2:
このループは4バイト毎に5クロックサイクルかかる。配列はもちろん4でアラインされているべきである。もし配列の要素数が4で割り切れなければ、少し余分なバイト数を後で埋め合せることで、長さを4で割り切れるようにすればよい。このループはいつでも配列の最後を読みすぎるので、一般保護例外を避けるために、必ず配列をセグメントの終わりには置かないようにするべきである。

各バイトの最上位ビットをマスクして、加算時に各バイトから次のバイトへのキャリーがあるかもしれないのを避けていることに注意してほしい。私は最上位ビットを戻すのに、ADDの代わりにXORを使って、キャリーを避けている。 ADD ESI,4 命令は、例4のようにループカウンタを添字に使うようにすれば避けることができただろう。しかしながら、これだとループ本体の命令数が奇数になるので、ペアにならない命令ができてループは依然として5クロックかかるだろう。分岐命令をペアにしないと、分岐が予測ミスしたときに最後の操作の後で1クロック節約できるが、配列の最後へのポインタを設定し、-Nを計算するための準備のコードで余分なクロックサイクルを消費しなければならないため、二つの方法はちょうど同じ速さになる。ここで示した方法は最も簡単で最も短い。

次の例は、最初の0のバイトを探すことで、0で終わる文字列の長さを調べる。これは REP SCASB を使うより速い。

例10:

STRLEN  PROC    NEAR
        MOV     EAX,[ESP+4]               ; ポインタを得る
        MOV     EDX,7
        ADD     EDX,EAX                   ; pointer+7 (最後に使う)
        PUSH    EBX
        MOV     EBX,[EAX]                 ; 最初の4バイトを読む
        ADD     EAX,4                     ; ポインタを増やす
L1:     LEA     ECX,[EBX-01010101H]       ; 各バイトから1を引く
        XOR     EBX,-1                    ; すべてのバイトを反転する
        AND     ECX,EBX                   ; これら二つのAND
        MOV     EBX,[EAX]                 ; 次の4バイトを読む
        ADD     EAX,4                     ; ポインタを増やす
        AND     ECX,80808080H             ; 符号ビットをすべてテスト
        JZ      L1                        ; 0のバイトはない、ループを続ける
        TEST    ECX,00008080H             ; 最初の二つのバイトをテスト
        JNZ     SHORT L2
        SHR     ECX,16                    ; 最初の二つのバイトにない
        ADD     EAX,2
L2:     SHL     CL,1                      ; 分岐を避けるためキャリーフラグを使う
        POP     EBX
        SBB     EAX,EDX                   ; 長さを計算
        RET                               ; (Pascalなら RET 4)
STRLEN  ENDP
ここでまた、ペアリングを改良するために、ある操作の最後を次の操作の最初とオーバーラップさせる方法を使った。比較的少ない回数しかループは繰り返されないだろうから、ループを伸ばすことはしなかった。コードはいつでも文字列の終わりを読み過ぎてしまうので、文字列はセグメントの終わりに置くべきでない。

ループ本体には奇数個の命令があり、そのためペアになっていないものが一つある。他の命令でなく分岐命令をペアにさせないことは、分岐が予測ミスしたときに1クロックサイクル節約できるという利点がある。

TEST ECX,00008080 命令はペアにできない。代わりにペアにできる命令 OR CH,CL を使うこともできたが、そうすると連続する分岐のペナルティーを避けるためにNOPか何かを入れなければならないだろう。 OR CH,CL の別の問題は、PProまたはPIIで部分レジスタストールを起こすだろうことである。それで私はペアにできないTEST命令を選んだ。

4バイトを同時に扱うのはかなり難しくなり得る。コードでは、バイトが0のとき、そのときに限り、0でない値を生成する式を使っている。これが4バイトすべてを一つの操作で調べることを可能にしている。このアルゴリズムはすべてのバイトから1を引く操作を含んでいる(LEA命令で)。引き算の前では、前の例のように最上位ビットをマスクすることはしなかったので、引き算は次のバイトへのボローを生成するかもしれないが、それはバイトが0のときに限る。そしてこれは、まさに我々が、次のバイトが何であるか気にしない状況である。もし逆方向に探すのなら、0を検出後にDWORDを読み直し、4バイトすべてを調べて最後の0を見つけるか、BSWAP命令を使ってバイト順序を逆転させなければならないだろう。

0以外のバイト値を探したいのなら、4バイトすべてを探している値でXORし、上の0を探す方法を使えばよい。

MMX操作を含むループ(PMMX)

同じレジスタで複数オペランドを扱うことは、MMXプロセッサでは簡単である。なぜならMMXプロセッサは、まさにこの目的の特別な命令と特別な64ビットレジスタを持っているからである。

配列のすべてのバイトに2を足す問題に戻ると、次のようにMMX命令の利点を生かすことができる。

例11:
.data
ALIGN   8
ADDENTS DQ      0202020202020202h       ; 加えるバイト値8回
A       DD      ?                       ; バイト配列の番地
N       DD      ?                       ; 繰り返し回数

.code
        MOV     ESI, [A]
        MOV     ECX, [N]
        MOVQ    MM2, [ADDENTS]
        JMP     SHORT L2
        ; ループの先頭
L1:     MOVQ    [ESI-8], MM0    ; 結果を格納
L2:     MOVQ    MM0, MM2        ; 被加算数をロード
        PADDB   MM0, [ESI]      ; 8バイトを1操作で足す
        ADD     ESI, 8
        DEC     ECX
        JNZ     L1
        MOVQ    [ESI-8], MM0    ; 最後の結果を格納
        EMMS

格納命令はループ制御命令の後に移動して、格納のストールを避けている。

PADDB命令は ADD ESI,8 とペアにならないので、このループは4クロックかかる。(メモリアクセスをするMMX命令は、MMXでない命令や、メモリアクセスをするもう一つのMMX命令とはペアになれない)。ECXを添字にすることで、 ADD ESI,8 を取り除くこともできるが、AGIストールが起きる。

ループのオーバーヘッドは考慮に値するので、ループを伸ばしたい。

例12:

.data
ALIGN   8
ADDENTS DQ      0202020202020202h       ; 加えるバイト値8回
A       DD      ?                       ; バイト配列の番地
N       DD      ?                       ; 繰り返し回数

.code
        MOVQ    MM2, [ADDENTS]
        MOV     ESI, [A]
        MOV     ECX, [N]
        MOVQ    MM0, MM2
        MOVQ    MM1, MM2
L3:     PADDB   MM0, [ESI]
        PADDB   MM1, [ESI+8]
        MOVQ    [ESI], MM0
        MOVQ    MM0, MM2
        MOVQ    [ESI+8], MM1
        MOVQ    MM1, MM2
        ADD     ESI, 16
        DEC     ECX
        JNZ     L3
        EMMS

この伸ばしたループは繰り返し毎に16バイトを加算するために6クロックかかる。PADD命令はペアにならない。二つのスレッドを交互に使って格納のストールを避けている。

後ですぐに浮動小数点命令を使う場合、MMX命令を使うことには高いペナルティーがあるので、例9のように32ビットレジスタを使いたい状況は依然としてあるだろう。

浮動小数点命令を含むループ

浮動小数点命令はペアになるのではなくオーバーラップするが、浮動小数点ループの最適化方法は、基本的には整数のループと同じである。

次のC言語のコードを考えてみよう:

  int i, n;  double * X;  double * Y;  double DA;
  for (i=0; i<n; i++)  Y[i] = Y[i] - DA * X[i];
このコード片(DAXPYと呼ばれる)は、線型方程式を解く鍵であるため、広く研究されてきた。
例13:

DSIZE   = 8                                      ; データサイズ
        MOV     EAX, [N]                         ; 要素数
        MOV     ESI, [X]                         ; Xへのポインタ
        MOV     EDI, [Y]                         ; Yへのポインタ
        XOR     ECX, ECX
        LEA     ESI, [ESI+DSIZE*EAX]             ; Xの終わりを指す
        SUB     ECX, EAX                         ; -N
        LEA     EDI, [EDI+DSIZE*EAX]             ; Yの終わりを指す
        JZ      SHORT L3                         ; N = 0 のテスト
        FLD     DSIZE PTR [DA]
        FMUL    DSIZE PTR [ESI+DSIZE*ECX]        ; DA * X[0]
        JMP     SHORT L2                         ; ループに飛び込む
L1:     FLD     DSIZE PTR [DA]
        FMUL    DSIZE PTR [ESI+DSIZE*ECX]        ; DA * X[i]
        FXCH                                     ; 前の結果を得る
        FSTP    DSIZE PTR [EDI+DSIZE*ECX-DSIZE]  ; Y[i] に格納
L2:     FSUBR   DSIZE PTR [EDI+DSIZE*ECX]        ; Y[i] から引く
        INC     ECX                              ; 添字を増加
        JNZ     L1                               ; ループ
        FSTP    DSIZE PTR [EDI+DSIZE*ECX-DSIZE]  ; 最後の結果を格納
L3:
ここでは例6と同じ方法―ループカウンタを添字のレジスタとして使い、負の値から0まで数える方法―を使っている。また、ある操作の最後は次の最初とオーバーラップする。

浮動小数点操作のインターリーブは、ここでは完全にうまくいっている。FMULとFSUBRの間の2クロックのストールを、前の結果のFSTPが埋めている。FSUBRとFSTPの間の3クロックのストールを、ループのオーバーヘッドと次の操作の最初の2命令が埋めている。添字が増加した後の最初のクロックサイクルで、添字に依存しない唯一のパラメタを読むことで、AGIストールを避けている。

この解は操作当たり6クロックサイクルかかり、インテルの公表しているループを伸ばした解より良い!

浮動小数点ループを伸ばす

3回伸ばしたDAXPYループはたいそうこみいっている:
例14:
DSIZE = 8                                 ; データサイズ
IF DSIZE EQ 4
SHIFTCOUNT = 2
ELSE
SHIFTCOUNT = 3
ENDIF

        MOV     EAX, [N]                  ; 要素数
        MOV     ECX, 3*DSIZE              ; カウンタのバイアス
        SHL     EAX, SHIFTCOUNT           ; DSIZE*N
        JZ      L4                        ; N = 0
        MOV     ESI, [X]                  ; Xへのポインタ
        SUB     ECX, EAX                  ; (3-N)*DSIZE
        MOV     EDI, [Y]                  ; Yへのポインタ
        SUB     ESI, ECX                  ; 終わりへのポインタ - バイアス
        SUB     EDI, ECX
        TEST    ECX, ECX
        FLD     DSIZE PTR [ESI+ECX]       ; 最初のX
        JNS     SHORT L2                  ; 操作は4回未満
L1:     ; main loop
        FMUL    DSIZE PTR [DA]
        FLD     DSIZE PTR [ESI+ECX+DSIZE]
        FMUL    DSIZE PTR [DA]
        FXCH
        FSUBR   DSIZE PTR [EDI+ECX]
        FXCH
        FLD     DSIZE PTR [ESI+ECX+2*DSIZE]
        FMUL    DSIZE PTR [DA]
        FXCH
        FSUBR   DSIZE PTR [EDI+ECX+DSIZE]
        FXCH    ST(2)
        FSTP    DSIZE PTR [EDI+ECX]
        FSUBR   DSIZE PTR [EDI+ECX+2*DSIZE]
        FXCH
        FSTP    DSIZE PTR [EDI+ECX+DSIZE]
        FLD     DSIZE PTR [ESI+ECX+3*DSIZE]
        FXCH
        FSTP    DSIZE PTR [EDI+ECX+2*DSIZE]
        ADD     ECX, 3*DSIZE
        JS      L1                        ; ループ
L2:     FMUL    DSIZE PTR [DA]            ; 残りの操作を済ませる
        FSUBR   DSIZE PTR [EDI+ECX]
        SUB     ECX, 2*DSIZE              ; ポインタのバイアスを変える
        JZ      SHORT L3                  ; 済んでいる
        FLD     DSIZE PTR [DA]            ; 次の操作を始める
        FMUL    DSIZE PTR [ESI+ECX+3*DSIZE]
        FXCH
        FSTP    DSIZE PTR [EDI+ECX+2*DSIZE]
        FSUBR   DSIZE PTR [EDI+ECX+3*DSIZE]
        ADD     ECX, 1*DSIZE
        JZ      SHORT L3                  ; 済んでいる
        FLD     DSIZE PTR [DA]
        FMUL    DSIZE PTR [ESI+ECX+3*DSIZE]
        FXCH
        FSTP    DSIZE PTR [EDI+ECX+2*DSIZE]
        FSUBR   DSIZE PTR [EDI+ECX+3*DSIZE]
        ADD     ECX, 1*DSIZE
L3:     FSTP    DSIZE PTR [EDI+ECX+2*DSIZE]
L4:

私がループを3回伸ばす方法を見せているのは、それを勧めるためではなくそれがいかに難しいかを警告するためである! このようなことをするときは、コードのデバッグや検証のためにけっこうな時間を使うことを覚悟してほしい。めんどうをみなければならない問題がいくつかある。ほとんどの場合、巻き込み(つまり、各実行の終わりで終わっていない操作があり、それは次の実行で終わる)を使わなければ、4回未満で伸ばした浮動小数点ループからすべてのストールを取り除くことはできない。上のメインループの最後のFLDは、次の実行の最初の命令である。ここでは、例9例10のように、配列の終わりを読み過ぎて、最後に余分な値を捨てるという解答をするのは、たいへん魅力的だろう。しかし、配列の後のメモリ位置が正当な浮動小数点数を含んでいない場合、余分な値を読むことで、デノーマルオペランド例外が発生するため、浮動小数点ループでは、これは勧められない。これを避けるためには、少なくとももう一つの操作をメインループの後でしなければならない。

伸ばしたループの外でする操作の数は、通常は、Nを操作の数、Rを伸ばす数として、 N MODULO R になるだろう。しかし巻き込んだループの場合は、上で述べた理由のために、もう一回、つまり (N-1) MODULO R + 1 回しなければならない。

通常は、メインループの前で余分な操作をするほうが望ましいだろうが、ここでは二つの理由によって、後でしなければならない。一つの理由は、巻き込みで残されたオペランドのめんどうを見ることである。もう一つの理由は、Rが2の冪でない場合、余分な操作の回数を計算するには、Rでの除算が必要であり、除算は時間がかかるからである。ループの後で余分な操作をすると、除算が節約できる。

次の問題は、どのようにループカウンタにバイアスをつけて、正しいときに符号が変わるようにするかを計算することと、このバイアスを補償するようにベースポインタを調整することである。最後の問題は、すべてのNの値について、巻き込みで残った操作が、必ず正しく扱えるようにしなければならないことである。

1〜3回の操作をする結びのコードは、別のループとして実装してもよかったが、分岐予測ミスのコストが追加されるだろうから、上の解のほうが速い。 3回伸ばすのがどんなに難しいかやってみせることで怖がらせてしまったので、今度は4回伸ばすとずっと簡単であることを見せよう。

例15:

DSIZE   = 8                               ; データサイズ
        MOV     EAX, [N]                  ; 要素数
        MOV     ESI, [X]                  ; Xへのポインタ
        MOV     EDI, [Y]                  ; Yへのポインタ
        XOR     ECX, ECX
        LEA     ESI, [ESI+DSIZE*EAX]      ; Xの終わりを指す
        SUB     ECX, EAX                  ; -N
        LEA     EDI, [EDI+DSIZE*EAX]      ; Yの終わりを指す
        TEST    AL,1                      ; Nが奇数か調べる
        JZ      SHORT L1
        FLD     DSIZE PTR [DA]            ; 半端な操作をする
        FMUL    DSIZE PTR [ESI+DSIZE*ECX]
        FSUBR   DSIZE PTR [EDI+DSIZE*ECX]
        INC     ECX                       ; カウンタを調整
        FSTP    DSIZE PTR [EDI+DSIZE*ECX-DSIZE]
L1:     TEST    AL,2                      ; さらに2回分操作するか調べる
        JZ      L2
        FLD     DSIZE PTR [DA]            ; N MOD 4 = 2 か 3 なのでもう2回する
        FMUL    DSIZE PTR [ESI+DSIZE*ECX]
        FLD     DSIZE PTR [DA]
        FMUL    DSIZE PTR [ESI+DSIZE*ECX+DSIZE]
        FXCH
        FSUBR   DSIZE PTR [EDI+DSIZE*ECX]
        FXCH
        FSUBR   DSIZE PTR [EDI+DSIZE*ECX+DSIZE]
        FXCH
        FSTP    DSIZE PTR [EDI+DSIZE*ECX]
        FSTP    DSIZE PTR [EDI+DSIZE*ECX+DSIZE]
        ADD     ECX, 2                    ; カウンタは4で割り切れる
L2:     TEST    ECX, ECX
        JZ      L4                        ; もう操作はない
L3:     ; main loop:
        FLD     DSIZE PTR [DA]
        FLD     DSIZE PTR [ESI+DSIZE*ECX]
        FMUL    ST,ST(1)
        FLD     DSIZE PTR [ESI+DSIZE*ECX+DSIZE]
        FMUL    ST,ST(2)
        FLD     DSIZE PTR [ESI+DSIZE*ECX+2*DSIZE]
        FMUL    ST,ST(3)
        FXCH    ST(2)
        FSUBR   DSIZE PTR [EDI+DSIZE*ECX]
        FXCH    ST(3)
        FMUL    DSIZE PTR [ESI+DSIZE*ECX+3*DSIZE]
        FXCH
        FSUBR   DSIZE PTR [EDI+DSIZE*ECX+DSIZE]
        FXCH    ST(2)
        FSUBR   DSIZE PTR [EDI+DSIZE*ECX+2*DSIZE]
        FXCH
        FSUBR   DSIZE PTR [EDI+DSIZE*ECX+3*DSIZE]
        FXCH    ST(3)
        FSTP    DSIZE PTR [EDI+DSIZE*ECX]
        FSTP    DSIZE PTR [EDI+DSIZE*ECX+2*DSIZE]
        FSTP    DSIZE PTR [EDI+DSIZE*ECX+DSIZE]
        FSTP    DSIZE PTR [EDI+DSIZE*ECX+3*DSIZE]
        ADD     ECX, 4                             ; 添字を4増加
        JNZ     L3                                 ; ループ
L4:

普通は4回伸ばすとストールのない解を見つけるのはまったく簡単である。メインループの外でする余分な操作の数は N MODULO 4 であり、単にNの下位2ビットを調べることで、除算なしで簡単に計算できる。ループカウンタの扱いを簡単にするために、余分な操作はメインループの後ではなく前に行う。

ループを伸ばすことのトレードオフは、ループの外の余分な操作が不完全なオーバーラップと分岐予測ミスのために遅く、増大したコードサイズのために初回のペナルティーが高くなることである。

一般的なお勧めとしては、Nが大きいか、伸ばすことなくループを巻き込んだときにストールを十分除去できなければ、決定的に重要な整数ループは2回、浮動小数点ループは4回伸ばすべきだと言っておこう。


17. 特殊な命令について

17.1 LEA

LEA命令は、シフトと二回の加算とデータの移動を、1クロックのペアにできる1命令だけでできるので、多くの目的に有用である。
例:
LEA EAX,[EBX+8*ECX-1000]
MOV EAX,ECX / SHL EAX,3 / ADD EAX,EBX / SUB EAX,1000
よりずっと速い。LEA命令はまた、フラグを変化させない加算あるいはシフトをするために使うこともできる。転送元と先のワードサイズは同じでなくてもよいので、 LEA EAX,[BX] は、 MOVZX EAX,BX の有用な置き換えである。

しかしながら、PPlainとPMMXでは、LEA命令は、前のクロックサイクルで変更されたベースレジスタまたはインデックスレジスタを使うと、AGIストールを受けることに、気をつけなければならない。

PPlainとPMMXでは、LEA命令はVパイプでペアにでき、シフト命令はできないので、Vパイプで命令を実行したいときに1、2または3でのシフトの置き換えに使ってもよい。

この32ビットプロセッサには、スケールされたインデックスレジスタ以外何もないような、文献に記されたアドレシングモードはない。そのため、 LEA EAX,[EAX*2] のような命令は、実際には4バイトの即値の変位をつけて、 LEA EAX,[EAX*2+00000000] としてコード化される。代わりに LEA EAX,[EAX+EAX] またはさらに良くして ADD EAX,EAX と書くことで、命令のサイズを縮小できる。後者のコードはAGIの遅れがない。もしたまたま値が0であるような(ループの後のループカウンタのような)レジスタがあれば、それをベースレジスタとして使ってコードサイズを縮小することもできる。

LEA EAX,[EBX*4]     ; 7バイト
LEA EAX,[ECX+EBX*4] ; 3バイト

17.2 MOV [MEM],ACCUM (PPlain and PMMX)

MOV [mem],AL と MOV [mem],AX と MOV [mem],EAX の三つの命令は、ペアリング回路で、アキュームレータに書き込むかのように扱われる。そのため、次の命令はペアにならない。
MOV [mydata], EAX
MOV EBX, EAX

この問題は、ベースレジスタやインデックスレジスタを持てず、転送元としてアキュームレータしか使えない、MOV命令の短い形でのみ起きる。別のレジスタを使う、命令を並べ変える、ポインタを使う、あるいはMOV命令の一般形を直にコード化することで、この問題を避けられる。

32ビットモードでは、 MOV [mem],EAX の一般形は次のように書ける。

DB 89H, 05H
DD OFFSET DS:mem

16ビットモードでは、 MOV [mem],AX の一般形は次のように書ける。

DB 89H, 06H
DW OFFSET DS:mem

(E)AXの代わりにALを使うには、89Hを88Hで置き換える。

このバグはMMX版でも直っていない。

17.3 TEST (PPlain and PMMX)

即値オペランドを持つTEST命令は、転送先がAL、AXまたはEAXのときのみペアにできる。

TEST レジスタ,レジスタ と TEST レジスタ,メモリ はいつでもペアにできる。

例:
TEST ECX,ECX                ; ペアにできる
TEST [mem],EBX              ; ペアにできる
TEST EDX,256                ; ペアにできない
TEST DWORD PTR [EBX],8000H  ; ペアにできない
ペアにできるようにするには、次の方法のどれかを使う。
MOV EAX,[EBX] / TEST EAX,8000H
MOV EDX,[EBX] / AND  EDX,8000H
MOV AL,[EBX+1] / TEST AL,80H
MOV AL,[EBX+1] / TEST AL,AL  ; (結果はサインフラグ)
キャリーフラグにシフトすることでビットをテストすることも可能である。
MOV EAX,[EBX] / SHR EAX,16   ; (結果はキャリーフラグ)

(このペアにできないことの原因はおそらく、この2バイト命令の最初のバイトが、何か他のペアにできない命令と同じで、プロセッサはペア可能性を決めるときに2番目のバイトもチェックする余裕がないことだろう。)

17.4 XCHG

XCHG レジスタ,[メモリ] 命令は危険である。この命令はデフォルトで暗黙のLOCKプリフィックスを持ち、そのせいでキャッシュの使用を禁止される。そのためこの命令はたいへん時間を消費し、いつでも避けるべきである。

17.5 キャリー経由のローテート(PPlain and PMMX)

1以外の回数のRCRとRCLは遅く、避けるべきである。

17.6 ストリング命令(PPlain and PMMX)

リピートプリフィックスなしのストリング命令は遅すぎる。いつでも単純な命令の列で置き換えるべきである。LOOPとJECXZも同様である。

リピートつきのストリング命令は、概ね最適である。可能ならいつでもDWORD版を使い、必ず転送元と転送先の両方を4でアラインする。

REP MOVSDは、転送先がレベル1キャッシュにあるときは、データブロックを転送する最も速い方法である。代わりの方法については19章を参照してほしい。REP MOVS命令がワードを転送先に書く間、同じクロックサイクルで次のワードを転送元から読むことに注意してほしい。規則10.3によれば、この二つの番地のビット2〜4が同じなら、キャッシュバンク競合が起きる。言い換えれば、ESI+(ワードサイズ)-EDIが32で割り切れれば、繰り返し毎に1クロック余計なペナルティーを受ける。キャッシュバンク競合を避ける最も簡単な方法は、DWORD版を使い、転送元と転送先を両方とも8でアラインすることである。最適化されたコードでは、たとえ16ビットモードでも、決してMOVSBとMOVSWを使ってはならない。

REP STOSDは、転送先がキャッシュにあるときは、最適である。

REP LODS、REP SCAS、REP CMPSは最適ではなく、ループで置き換えてもよい。REP SCASBの代わりについては16章例10を参照してほしい。REP CMPSは、ESIとEDIのビット2〜4が同じなら、キャッシュバンク競合を受ける。

17.7 ビットスキャン(PPlain and PMMX)

BSFとBSRは、PPlainとPMMXで最適化の最も貧弱な命令であり、nをスキップする0の数として、だいたい11+2*nクロックサイクルかかる。

次のコードは BSR ECX,EAX をエミュレートする。

        TEST    EAX,EAX
        JZ      SHORT BS1
        MOV     DWORD PTR [TEMP],EAX
        MOV     DWORD PTR [TEMP+4],0
        FILD    QWORD PTR [TEMP]
        FSTP    QWORD PTR [TEMP]
        WAIT    ; WAITは古いプロセッサとの互換性のためだけに必要
        MOV     ECX, DWORD PTR [TEMP+4]
        SHR     ECX,20	      ; 指数部を分離
        SUB     ECX,3FFH      ; 調整
        TEST    EAX,EAX       ; ゼロフラグを降ろす
BS1:

次のコードは BSF ECX,EAX をエミュレートする。

        TEST    EAX,EAX
        JZ      SHORT BS2
        XOR     ECX,ECX
        MOV     DWORD PTR [TEMP+4],ECX
        SUB     ECX,EAX
        AND     EAX,ECX
        MOV     DWORD PTR [TEMP],EAX
        FILD    QWORD PTR [TEMP]
        FSTP    QWORD PTR [TEMP]
        WAIT    ; WAITは前のプロセッサとの互換性のためだけに必要
        MOV     ECX, DWORD PTR [TEMP+4]
        SHR     ECX,20
        SUB     ECX,3FFH
        TEST    EAX,EAX       ; ゼロフラグを降ろす
BS2:

これらのエミュレーションコードはPProとPIIで使うべきではない。ビットスキャン命令は1または2クロックしかかからないし、上に示したエミュレーションコードには、部分メモリストールが二つある。

17.8 ビットテスト(PPlain and PMMX)

BT、BTC、BTR、BTS命令は、なるべくTEST、AND、OR、XOR、またはシフトのような命令で置き換えるべきである。

17.9 整数の乗算

整数の乗算は9クロックサイクルかかる。そのため、定数による乗算を他の、SHL、ADD、SUB、そしてLEAのような命令で置き換えるのが有利である。
例:
IMUL EAX,10
は
MOV EBX,EAX / ADD EAX,EAX / SHL EBX,3 / ADD EAX,EBX
または
LEA EAX,[EAX+4*EAX] / ADD EAX,EAX
で置き換えられる。

PPlainとPMMXでは、浮動小数点乗算は整数乗算より高速だが、普通は、浮動小数点乗算を使うことによる時間の節約より、整数を浮動小数点数に変換し、結果を変換し戻すのり使う時間のほうが多い。例外は乗算の数に比べて変換の数が少ないときである。MMX乗算は速いが、16ビットオペランドしか使えない。

17.10 除算

除算はたいそう時間を消費する。PPlainとPMMXでは、DIV命令はバイト、ワード、そしてダブルワードの除数についてそれぞれ、17、25、または41クロックサイクルかかる。IDIV命令はさらに5クロックサイクルかかる。従って、たとえオペランドサイズプリフィックスのコストを払ってでも、オーバーフローを生成しないできるだけ最小のオペランドサイズを、そして可能なら符号なし除算を使うのがよい。

定数による除算

2の冪による符号なし除算はSHRでできる。2の冪による符号つき除算はSARでできるが、SARの結果は負の無限大方向に丸められ、一方、IDIVの結果はゼロ方向に切り捨てられる。

定数による除算は逆数を掛けることでできる。 q = x/d を計算するには、まず逆数 f = 2^r / dを計算する(ただしrは2進の小数点(位取り点)を定義)。次に、xにfを掛け、右にrだけシフトする。rの最大値は、bをdの2進での桁数-1として、32+bである。(bは 2^b <= d となる最大の整数である)。被除数xの値の最大範囲をカバーするには r = 32+b を使う。

この方法は、丸め誤差を補償するために洗練する必要がある。次のアルゴリズムは、符号なし整数の切り捨て除算の正しい結果、つまりDIV命令のしているのと同じものを与える。

  b = dの有効ビット数 - 1
  r = 32 + b
  f = 2^r / d
  fが整数なら、dは2の冪: 場合Aに行く。
  fが整数でないなら、fの小数部が0.5未満か調べる。
  fの小数部 < 0.5: 場合Bに行く。
  fの小数部 > 0.5: 場合Cに行く。

  場合A: (d = 2^b)
  結果 = x SHR b

  場合B: (fの小数部 < 0.5)
  fを最も近い整数に丸める
  結果 = ((x+1) * f) SHR r

  場合C: (fの小数部 > 0.5)
  fを最も近い整数に丸める
  result = (x * f) SHR r
例:
5で割りたいとする。
5 = 00000101b.
b = 有効2進桁数 - 1 = 2
r = 32+2 = 34
f = 2^34 / 5 = 3435973836.8 = 0CCCCCCCC.CCC...(16進)
小数部は1/2より大きい: 場合Cを使う。
fを切り上げて0CCCCCCCDhとする。

このコードはEAXを5で割って、結果をEDXに返す:

        MOV     EDX,0CCCCCCCDh
        MUL     EDX
        SHR     EDX,2

乗算後、EDXには積を32ビット右にシフトしたものが入っている。r=34なので、もう2だけシフトしなければならない。10で割るには、最後の行を SHR EDX,3 に変えるだけでよい。

場合Bでは、次のようになるだろう:

        INC     EAX
        MOV     EDX,f
        MUL     EDX
        SHR     EDX,b

このコードはすべてのxの値、ただし、INC命令のオーバーフローで0になってしまう0FFFFFFFFHを除く、でうまく動く。もしx=0FFFFFFFFHになり得るのなら、コードを変更して次のようにする:

        MOV     EDX,f
        ADD     EAX,1
        JC      DOVERFL
        MUL     EDX
DOVERFL:SHR     EDX,b

xの値が限られているのなら、rの小さい値、つまり桁数の少ないものを使ってもよい。rの小さい値を使う理由はいくつかある。

この場合のxの最大値は少なくとも2^(r-b)であり、時にはもっと大きい。コードが正しく動くxの最大値をちょうど知りたいなら、系統的なテストをしなければならない。

段落17.9で説明したように、遅い乗算を速い命令で置き換えたいかもしれない。

次の例はEAXを10で割って結果をEAXに返す。私はr=19の代わりにr=17を選んだ。たまたま最適化しやすいコードになり、xの同じ範囲をカバーするからである。

f = 2^17 / 10 = 3333h, 場合B: q = (x+1)*3333h:

        LEA     EBX,[EAX+2*EAX+3]
        LEA     ECX,[EAX+2*EAX+3]
        SHL     EBX,4
        MOV     EAX,ECX
        SHL     ECX,8
        ADD     EAX,EBX
        SHL     EBX,8
        ADD     EAX,ECX
        ADD     EAX,EBX
        SHR     EAX,17
系統的なテストでは、このコードはx<10004Hとなるすべてのxで正しく動くことが示された。

同じ値で繰り返し除算

もし除数がアセンブル時にはわからないが、同じ除数で繰り返し割るのなら、上と同じ方法が使える。コードは除算の前に場合A,B,Cを見分け、fを計算しなければならない。

次のコードは、同じ除数でのたくさんの除算(符号なし、切り捨て)のしかたを示す。最初に、除数を指定して逆数を計算するためにSET_DIVISORを呼び、次に同じ除数で割る各値についてDIVIDE_FIXEDを呼ぶ。

.data

RECIPROCAL_DIVISOR DD ?                ; 除数の逆数を丸めたもの
CORRECTION         DD ?                ; 場合A: -1, 場合B: 1, 場合C: 0
BSHIFT             DD ?                ; 除数のビット数 - 1

.code

SET_DIVISOR PROC NEAR                  ; EAXに除数
        PUSH    EBX
        MOV     EBX,EAX
        BSR     ECX,EAX                ; b = 除数のビット数 - 1
        MOV     EDX,1
        JZ      ERROR                  ; エラー: 除数は0
        SHL     EDX,CL                 ; 2^b
        MOV     [BSHIFT],ECX           ; bを保存
        CMP     EAX,EDX
        MOV     EAX,0
        JE      SHORT CASE_A           ; 除数は2の冪
        DIV     EBX                    ; 2^(32+b) / d
        SHR     EBX,1                  ; 除数 / 2
        XOR     ECX,ECX
        CMP     EDX,EBX                ; 余りと除数/2を比較
        SETBE   CL                     ; 場合Bなら1
        MOV     [CORRECTION],ECX       ; 丸め誤差の補正
        XOR     ECX,1
        ADD     EAX,ECX                ; 場合Cなら1を加算
        MOV     [RECIPROCAL_DIVISOR],EAX ; 除数の逆数を丸めたもの
        POP     EBX
        RET
CASE_A: MOV     [CORRECTION],-1        ; 場合Aであることを覚えておく
        POP     EBX
        RET
SET_DIVISOR     ENDP

DIVIDE_FIXED PROC NEAR                 ; EAXに被除数、結果はEAX
        MOV     EDX,[CORRECTION]
        MOV     ECX,[BSHIFT]
        TEST    EDX,EDX
        JS      SHORT DSHIFT           ; 除数は2の冪
        ADD     EAX,EDX                ; 丸め誤差の補正
        JC      SHORT DOVERFL          ; オーバーフローの補正
        MUL     [RECIPROCAL_DIVISOR]   ; 除数の逆数を掛ける
        MOV     EAX,EDX
DSHIFT: SHR     EAX,CL                 ; 桁の調整
        RET
DOVERFL:MOV     EAX,[RECIPROCAL_DIVISOR] ; 被除数 = 0FFFFFFFFH
        SHR     EAX,CL                 ; シフトで除算
        RET
DIVIDE_FIXED    ENDP

このコードは、 0 <= x < 2^32, 0 < d < 2^32 についてDIV命令と同じ結果を与える。

注意: x < 0FFFFFFFFH であると確信できるなら、 JC DOVERFL とそのジャンプ先は不要である。

2の冪であることがほとんど起きず、最適化に値しないならば、DSHIFTへのジャンプを除いて、代わりに場合Aのときは CORRECTION = 0 で乗算をするようにしてもよい。

もし除数が頻繁に変わって、SET_DIVISORに最適化が必要なら、BSR命令を段落17.7で与えたコードで置き換えてもよい。

浮動小数点除算

PPlainとPMMXでは、浮動小数点除算は、最高精度では39クロックサイクルかかる。浮動小数点制御ワードで低い精度を指定することで、時間を節約できる(FDIVとFIDIVだけが低い精度で高速化し、他の命令はそうならない)。

浮動小数点除算と整数除算を並列に行って時間を節約することも可能である。

例: A = A1 / A2;  B = B1 / B2

        FILD    [B1]
        FILD    [B2]
        MOV     EAX, [A1]
        MOV     EBX, [A2]
        CDQ
        FDIV
        DIV     EBX
        FISTP   [B]
        MOV     [A], EAX
(浮動小数点制御ワードを望みの丸め方法に設定したか確かめよ)

いつも除算の数を最小にしようとするべきであることは明らかである。定数による浮動小数点除算や、同じ値による繰り返し除算はもちろん逆数の乗算で行うべきである。しかし除算の数を少なくできる状況は他にもたくさんある。例えば、 if (A/B > C)... はBが正なら if (A > B*C)... に、Bが負ならその逆に書き換えられる。

A/B + C/D は (A*D + C*B) / (B*D) に書き換えられる。

もし整数除算を使っているなら、式を書き換えると丸め誤差が異なることを知っているべきである。

17.11 WAIT

WAIT命令を省くことでしばしば、速度を上げることができる。WAIT命令は三つの働きがある。
  1. 古い8087プロセッサは、必ずコプロセッサが命令を受け取れるようにするため、すべての浮動小数点命令の前にWAIT命令を要求する。
  2. WAIT命令は、浮動小数点ユニットと整数ユニットの間のメモリアクセスを調整するために使われる。
    例:
    b.1.  FISTP [mem32]
          WAIT             ; 整数ユニットで結果を読む前に
          MOV EAX,[mem32]  ; 浮動小数点ユニットが書き込むのを待つ
    
    b.2.  FILD [mem32]
          WAIT             ; 整数ユニットで上書きする前に
          MOV [mem32],EAX  ; 浮動小数点ユニットが値を読むのを待つ
    
    b.3.  FLD QWORD PTR [ESP]
          WAIT             ; 偶発的なハードウェア割り込みがスタックの値を
          ADD ESP,8        ; 読まれる前に上書きするのを防ぐ
    
  3. WAITはときどき、例外の検査に使われる。浮動小数点ステータスワードのマスクされない例外ビットが、先立つ浮動小数点命令でセットされたときに、割り込みを生成するのである。

aについて:
aの働きは、古い8087以外のプロセッサには決して必要ない。コードを8087互換にしたいのでなければ、アセンブラに対して上位プロセッサを指定することで、このWAITを入れないように告げるべきである。8087浮動小数点エミュレータもWAIT命令を挿入する。従って、必要なければ、エミュレーションコードを生成しないようにアセンブラに告げるべきである。

bについて:
メモリアクセスを調整するWAIT命令は、8087と80287では絶対必要だが、Pentiumではそうではない。80387と80486で必要かどうかはあまりはっきりしない。私は何回かこれらのインテルプロセッサ上でテストを行ったが、インテルのマニュアルには、FNSTSWとFNSTCWの後を除いてこの目的のWAITは必要と書いてあるにもかかわらず、インテルのどの32ビットプロセッサでも、WAITを省くことによるエラーを発生させることはできなかった。32ビットコードを書いているときでさえ、メモリアクセスを調整するWAIT命令を省くことは100%安全ではない。なぜなら、コードは287コプロセッサつきの80386メインプロセッサで走るかもしれず、そのときはWAITが必要だからである。しかも、私はすべてのハードウェアとソフトウェアの組合せを試したわけではないので、このWAITの必要な、他の状況があるかもしれない。

どんな32ビットプロセッサ(インテル以外のプロセッサも含む)でもコードが確かに動くようにしたいのなら、安全のためにこのWAITを入れることを勧める。

cについて:
アセンブラは、次に示す命令: FCLEX, FINIT, FSAVE, FSTCW, FSTENV, FSTSW の前に、この目的のためのWAITを自動的に入れる。このWAITはFNCLEXなどのように書くことで省ける。私のテストでは、WAITなしのこれらの命令でも、80387ではFNCLEXとFNINITを除いて例外の際に割り込みを生成するので、ほとんどの場合WAITは必要ない。(割り込みからのIRETがFN..命令を指すか次の命令を指すかについて不整合がある)。

他の浮動小数点命令もほとんどすべて、以前の浮動小数点命令がマスクされていない例外ビットをセットしていれば、割り込みを生成するので、例外は遅かれ早かれいずれ検出される傾向にある。プログラムの最後の浮動小数点命令の後にWAITを入れて、すべての例外を確かにつかまえるようにしてもよい。

例外が正確にどこで発生したかを知って、その状況から回復できるようにしたいのなら、WAITはやはり必要である。例えば上のb.3のコードを考えてみよう。ここのFLDが生成する例外から回復できるようにしたいなら、 ADD ESP,8 の後の割り込みはロードされる値を上書きしてしまうので、WAITが必要である。

17.12 FCOM + FSTSW AX

PProとPIIプロセッサは遅いFSTSWを避けるためのFCOMI命令を持っている。FCOMI命令のないプロセッサでは、浮動小数点比較をする普通のやり方はこうである。
FLD [a]
FCOMP [b]
FSTSW AX
SAHF
JB ASmallerThanB

FSTSW AX の代わりに FNSTSW AX を使い、ペアにできないSAHFを使う代わりにAHを直接テストすることで、このコードを改良できる。(TASM Version 3.0 は FNSTSW AX 命令にバグがある)

FLD [a]
FCOMP [b]
FNSTSW AX
SHR AH,1
JC ASmallerThanB
ゼロかどうか、または等しいかどうかテストする:
FTST
FNSTSW AX
AND AH,40H
JNZ IsZero     ; (ゼロフラグは反転している!)
大きいかどうかテストする:
FLD [a]
FCOMP [b]
FNSTSW AX
AND AH,41H
JZ AGreaterThanB

TEST AH,41H は、ペアにできないので使ってはいけない。 TEST EAX,4100H は、PProとPIIで部分レジスタストールを起こすので使ってはいけない。

PPlainとPMMXでは、FNSTSW命令はどんな浮動小数点命令の後でも4クロック遅れる(おそらくステータスワードがパイプラインからリアイアするのを待っているからだろう)。このFCOMからFNSTSWまでのレイテンシーを整数命令で埋められるなら、FNSTSW命令は6クロックではなく2クロックしかかからない。この場合、必ずWAITプリフィックスのないバージョンを使うようにせよ。

時には、18章で述べるように、浮動小数点の値の比較に整数命令を使うほうが速い。

17.13 FLDCW (PPro and PII)

PProとPIIは、FLDCW命令に、制御ワードを読むような浮動小数点命令(たいていの浮動小数点命令はそう)が続いていると、その後で深刻なストールがある。

CまたはC++のコードがコンパイルされると、浮動小数点数を整数に変換するのは切り捨てで行うが、他の浮動小数点演算では丸めを行うので、FLDCW命令をたくさん生成する。アセンブリ言語に変換された後で、可能な場所では切り捨ての代わりに丸めを使ったり、中で切り捨てが必要なループからFLDCWを外に移動したりすることで、コードを改良できる。

17.14 浮動小数点レジスタの解放 (PPlain and PMMX)

サブルーチンから出る前には、使った浮動小数点レジスタを、結果に使うレジスタを除いてすべて解放しなければならない。

一つのレジスタを解放する最も速い方法は、 FSTP ST である。
二つのレジスタを解放する最も速い方法は、 FCOMPP である。
FFREEを使うことは勧められない。

17.15 FISTP (PPlain and PMMX)

浮動小数点数を整数に変換するのは、普通次のようにする。
        FISTP   DWORD PTR [TEMP]
        MOV     EAX, [TEMP]
代わりの方法として次のものがある。
.DATA
ALIGN 8
TEMP    DQ      ?
MAGIC   DD      59C00000H   ; 2^51 + 2^52 の浮動小数点表現
.CODE
        FADD    [MAGIC]
        FSTP    QWORD PTR [TEMP]
        MOV     EAX, DWORD PTR [TEMP]
「マジックナンバー」 2^51 + 2^52 を加えることは、-2^31と+2^31の間のどんな整数も、倍精度浮動小数点数として格納されるときに下位32ビットに整合されるという効果がある。ゼロ方向に切り捨てるのを除くすべての丸め方法で、FISTPと結果は同じである。制御ワードが切り捨てを指定しているときと、オーバーフローの場合は、結果はFISTPと異なる。古いプロセッサとの互換性のためにWAIT命令が必要かもしれない。段落17.11を参照せよ。

この方法はFISTPを使うより速くはないが、FADDとFSTPの間に3クロックの空きがあって、他の命令で埋められるかもしれないので、よりうまいスケジューリングの機会を提供する。同じ操作で数を2の冪で掛けたり割ったりするのを、マジックナンバーに逆の操作をすることで行える。マジックナンバーに数を加えておくことで、定数の加算もできるが、このときマジックナンバーは倍精度にしなければならない。

17.16 FPTAN

マニュアルによれば、FPTANは二つの値XとYを返し、YをXで割って結果を得るのをプログラマに任せているが、実はFPTANはいつでもXに1を返すので、除算を節約できる。私のテストでは、浮動小数点ユニットまたはコプロセッサを持つすべての32ビットインテルプロセッサで、FPTANは引数によらずいつでもXに1を返す。もし、コードがすべてのプロセッサで正しく走ることを絶対的に確かにしたいのなら、Xが1かどうかテスト(これはXで割るより速い)すればよい。Yの値は非常に大きいかもしれないが、決して無限大ではないので、引数が正当であることを知っているなら、Yが正当な数であることをテストしなくてよい。

18. 整数命令を使った浮動小数点演算(PPlain and PMMX)

整数命令は概ね、浮動小数点命令より速い。だから、単純な浮動小数点操作をするには、整数命令を使うほうが有利なことがしばしばある。最も明らかな例はデータの移動である。
例:
FLD QWORD PTR [ESI] / FSTP QWORD PTR [EDI]
を変更して
MOV EAX,[ESI] / MOV EBX,[ESI+4] / MOV [EDI],EAX / MOV [EDI+4],EBX
にする。
前者のコードは4クロックかかるが、後者は2クロックである。

浮動小数点値がゼロかどうかテスト:
浮動小数点値のゼロは普通、32ビットまたは64ビットのゼロで表されるが、これには落とし穴がある。符号ビットがセットされているかもしれない! マイナスゼロは有効な浮動小数点数とみなされ、プロセッサは実際、例えば負の数とゼロを掛けると符号ビットがセットされたゼロを生成する。だから、浮動小数点数がゼロかどうかテストしたいなら、符号ビットをテストしないようにするべきである。

例:
FLD DWORD PTR [EBX] / FTST / FNSTSW AX / AND AH,40H / JNZ IsZero
代わりに整数命令を使って、符号ビットをシフトで追い出そう。
MOV EAX,[EBX] / ADD EAX,EAX / JZ IsZero
前者のコードは9クロックかかるが、後者はたった2クロックである。 浮動小数点数が倍精度(QWORD)なら、ビット32-62だけ調べればよい。もしそれらがゼロなら、もし普通の浮動小数点数なら下位半分もゼロなのである。

負かどうかテスト:
符号ビットがセットされていて、少なくとも一つの他のビットがセットされていれば、浮動小数点数は負である。

例:
MOV EAX,[NumberToTest] / CMP EAX,80000000H / JA IsNegative

符号ビットの操作:
浮動小数点数の符号を変えるには、単に符号ビットを反転させればよい。

例:
XOR BYTE PTR [a] + (TYPE a) - 1, 80H

同様に、単に符号ビットをANDするだけで浮動小数点数の絶対値が得られる。

数の比較:
浮動小数点数は、符号ビットを除いて、整数命令で浮動小数点数を比較できるようなユニークな形式で格納される。二つの浮動小数点数が通常の数で正であると確信できるなら、単に整数として比較してよい。

例:
FLD [a] / FCOMP [b] / FNSTSW AX / AND AH,1 / JNZ ASmallerThanB
を変更して
MOV EAX,[a] / MOV EBX,[b] / CMP EAX,EBX / JB ASmallerThanB
にする。
この方法は、二つの数が同じ精度で、どちらの数の符号ビットもセットされていないと確信できるときだけうまくいく。

もし負の数かもしれないときは、負の数を二の補数に変換して、符号つき比較をしなければならない。

        MOV     EAX, [a]
        MOV     EBX, [b]
        MOV     ECX, EAX
        MOV     EDX, EBX
        SAR     ECX, 31              ; 符号ビットをコピー
        AND     EAX, 7FFFFFFFH       ; 符号ビットを除去
        SAR     EDX, 31
        AND     EBX, 7FFFFFFFH
        XOR     EAX, ECX             ; 符号ビットがセットされていたら、二の補数にする
        XOR     EBX, EDX
        SUB     EAX, ECX
        SUB     EBX, EDX
        CMP     EAX, EBX
        JL      ASmallerThanB        ; 符号つき比較
この方法は通常の浮動小数点数すべて(-0を含む)でうまくいく。

19. 浮動小数点命令を使った整数演算(PPlain and PMMX)

19.1 データの移動(PPlain and PMMX)

浮動小数点命令は、8バイトを一度に移動するために使うことができる:
FILD QWORD PTR [ESI] / FISTP QWORD PTR [EDI]
これは転送先がキャッシュにないときのみ有利である。PPlainで、データブロックをキャッシュされていないメモリに移動する最適な方法は、次の通りである。
TOP:    FILD    QWORD PTR [ESI]
        FILD    QWORD PTR [ESI+8]
        FXCH
        FISTP   QWORD PTR [EDI]
        FISTP   QWORD PTR [EDI+8]
        ADD     ESI, 16
        ADD     EDI, 16
        DEC     ECX
        JNZ     TOP

転送元と先はもちろん8でアラインされているべきである。遅いFILDとFISTP命令で使われる余分な時間は、書き込み操作の数が半分でよいという事実によって補償される。この方法はPPlainとPMMXにおいてのみ、そして転送先がレベル1キャッシュにないときのみ、有利であることに注意せよ。他のすべてのプロセッサでは、REP MOVSDを使うほうが速い。

MMXプロセッサでは、8バイトを一度に移動するにはMMX命令を使うほうが速い。

TOP:    MOVQ    MM0,[ESI]
        MOVQ    [EDI],MM0
        ADD     ESI,8
        ADD     EDI,8
        DEC     ECX
        JNZ     TOP

キャッシュミスが予期されるなら、このループを伸ばしたりさらに最適化したりする必要は全くない。なぜなら、ここでは命令の実行ではなくメモリアクセスがボトルネックだからである。

19.2 整数の乗算(PPlain and PMMX)

PPlainとPMMXでは、浮動小数点乗算は整数乗算より速いが、整数を浮動小数点数に、そして結果を整数に戻す変換の値段は高いので、浮動小数点演算は必要な変換の数が乗算の数と比べて少ないときのみ有利である。(ここの変換を少しでも節約するためにデノーマル浮動小数点オペランドを使うのは誘惑的かもしれないが、デノーマルの扱いはたいへん遅いため、これはよいアイデアではない!)

PMMXでは、MMX乗算命令は整数乗算より速く、クロックサイクルあたり1回の乗算のスループットでパイプライン動作できるので、16ビット精度でよいのなら、これがPMMXで高速に乗算を行う最良の解決策かもしれない。 整数乗算は、PProとPIIでは浮動小数点乗算より速い。

19.3 整数除算(PPlain and PMMX)

浮動小数点除算は整数除算より速くないが、浮動小数点ユニットが除算で働いている間に、他の整数演算(整数除算を含むが、整数乗算は含まない)ができる。例は段落17.10を参照せよ。

19.4 2進数を10進数に変換

FBSTP命令を使うのが、必ずしも一番速くはないが、2進数を10に変換する、単純で便利な方法である。

20. 整数命令のリスト(PPlain and PMMX)

説明:
オペランド: r=レジスタ, m=メモリ, i=即値データ, sr=セグメントレジスタ, m32=32ビットメモリオペランド、など

クロックサイクル:
数字は最小値である。キャッシュミスやミスアラインメントや例外により、クロックカウントはだいぶ増大する。

ペア可能性:
u=Uパイプでペアにできる, v=Vパイプでペアにできる, uv=どちらのパイプでもペアにできる, np=ペアにできない

Opcode                 Operands            Clock cycles        Pairability
----------------------------------------------------------------------------
NOP                                        1                   uv
MOV                    r/m, r/m/i          1                   uv
MOV                    r/m, sr             1                   np
MOV                    sr , r/m            >= 2 b)             np
MOV                    m  , accum          1                   uv h)
XCHG                   (E)AX, r            2                   np
XCHG                   r  ,   r            3                   np
XCHG                   r  ,   m            >15                 np
XLAT                                       4                   np
PUSH                   r/i                 1                   uv
POP                    r                   1                   uv
PUSH                   m                   2                   np
POP                    m                   3                   np
PUSH                   sr                  1 b)                np
POP                    sr                  >= 3 b)             np
PUSHF                                      3-5                 np
POPF                                       4-6                 np
PUSHA POPA                                 5-9 i)              np
PUSHAD POPAD                               5                   np
LAHF SAHF                                  2                   np
MOVSX MOVZX            r, r/m              3 a)                np
LEA                    r/m                 1                   uv
LDS LES LFS LGS LSS    m                   4 c)                np
ADD SUB AND OR XOR     r  , r/i            1                   uv
ADD SUB AND OR XOR     r  , m              2                   uv
ADD SUB AND OR XOR     m  , r/i            3                   uv
ADC SBB                r  , r/i            1                   u
ADC SBB                r  , m              2                   u
ADC SBB                m  , r/i            3                   u
CMP                    r  , r/i            1                   uv
CMP                    m  , r/i            2                   uv
TEST                   r  , r              1                   uv
TEST                   m  , r              2                   uv
TEST                   r  , i              1                   f)
TEST                   m  , i              2                   np
INC DEC                r                   1                   uv
INC DEC                m                   3                   uv
NEG NOT                r/m                 1/3                 np
MUL IMUL               r8/r16/m8/m16       11                  np
MUL IMUL               all other versions  9 d)                np
DIV                    r8/m8               17                  np
DIV                    r16/m16             25                  np
DIV                    r32/m32             41                  np
IDIV                   r8/m8               22                  np
IDIV                   r16/m16             30                  np
IDIV                   r32/m32             46                  np
CBW CWDE                                   3                   np
CWD CDQ                                    2                   np
SHR SHL SAR SAL        r  , i              1                   u
SHR SHL SAR SAL        m  , i              3                   u
SHR SHL SAR SAL        r/m, CL             4/5                 np
ROR ROL RCR RCL        r/m, 1              1/3                 u
ROR ROL                r/m, i(><1)         1/3                 np
ROR ROL                r/m, CL             4/5                 np
RCR RCL                r/m, i(><1)         8/10                np
RCR RCL                r/m, CL             7/9                 np
SHLD SHRD              r, i/CL             4 a)                np
SHLD SHRD              m, i/CL             5 a)                np
BT                     r, r/i              4 a)                np
BT                     m, i                4 a)                np
BT                     m, r                9 a)                np
BTR BTS BTC            r, r/i              7 a)                np
BTR BTS BTC            m, i                8 a)                np
BTR BTS BTC            m, r                14 a)               np
BSF BSR                r  , r/m            7-73 a)             np
SETcc                  r/m                 1/2 a)              np
JMP CALL               short/near          1   e)              v
JMP CALL               far                 >= 3 e)             np
conditional jump       short/near          1/4/5/6 e)          v
CALL JMP               r/m                 2/5 e)              np
RETN                                       2/5 e)              np
RETN                   i                   3/6 e)              np
RETF                                       4/7 e)              np
RETF                   i                   5/8 e)              np
J(E)CXZ                short               4-11 e)             np
LOOP                   short               5-10 e)             np
BOUND                  r  , m              8                   np
CLC STC CMC CLD STD                        2                   np
CLI STI                                    6-9                 np
LODS                                       2                   np
REP LODS                                   7+3*n    g)         np
STOS                                       3                   np
REP STOS                                   10+n     g)         np
MOVS                                       4                   np
REP MOVS                                   12+n     g)         np
SCAS                                       4                   np
REP(N)E SCAS                               9+4*n    g)         np
CMPS                                       5                   np
REP(N)E CMPS                               8+4*n    g)         np
BSWAP                                      1 a)                np
CPUID                                      13-16    a)         np
RDTSC                                      6/8 a) j)           np
----------------------------------------------------------------------------
注:
a) この命令は0FHプリフィックスを持ち、複数サイクル命令が先行しなければ、PPlainではデコードのために1クロック余計にかかる(13章参照)。
b) FSとGSのときは0FHプリフィックスを持つ。注a参照。
c) SS,FS,GSのときは0FHプリフィックスを持つ。注a参照。
d) 2オペランドで即値なしのときは0FHプリフィックスを持つ。注a参照。
e) 12章参照。
f) レジスタがアキュームレータのときのみペアにできる。段落17.3参照。
g) CLDのような複数サイクル命令が先行しなければ、リピートプリフィックスのデコードのために1クロック追加(13章参照)。
h) ペアになるとき、アキュームレータに書き込むかのように扱われる。段落17.2参照。
i) SPが4で割り切れるときには9。10.7節参照。
j) PPlainでは6、PMMXでは8。

21. 浮動小数点命令のリスト

説明:
オペランド: r=レジスタ, m=メモリ, m32=32ビットメモリオペランド、など

クロックサイクル:
数字は最小値である。キャッシュミス、ミスアラインメント、デノーマルオペランドや例外により、クロックカウントはだいぶ増大する。

ペア可能性:
+=FXCHとペアにできる, np=FXCHとペアにできない

i-ov:
整数命令とのオーバーラップ。i-ov=4とは、最後の4クロックサイクルが引き続く整数命令とオーバーラップできることを意味する。

fp-ov:
浮動小数点命令とのオーバーラップ。fp-ov=2とは、最後の2クロックサイクルが引き続く浮動小数点命令とオーバーラップできることを意味する。(WAITは、ここでは浮動小数点命令と考える)

Opcode               Operand     Clock cycles    Pairability    i-ov   fp-ov
-----------------------------------------------------------------------------
FLD                  r/m32/m64         1         +              0      0
FLD                  m80               3         np             0      0
FBLD                 m80           48-58         np             0      0
FST(P)               r                 1         np             0      0
FST(P)               m32/m64           2 m)      np             0      0
FST(P)               m80               3 m)      np             0      0
FBSTP                m80         148-154         np             0      0
FILD                 m                 3         np             2      2
FIST(P)              m                 6         np             0      0
FLDZ FLD1                              2         np             0      0
FLDPI FLDL2E etc.                      5         np             0      0
FNSTSW               AX/m16            6 q)      np             0      0
FLDCW                m16               8         np             0      0
FNSTCW               m16               2         np             0      0

FADD(P)              r/m               3         +              2      2
FSUB(R)(P)           r/m               3         +              2      2
FMUL(P)              r/m               3         +              2      2 n)
FDIV(R)(P)           r/m        19/33/39 p)      +             38 o)   2
FCHS FABS                              1         +              0      0
FCOM(P)(P) FUCOM     r/m               1         +              0      0
FIADD FISUB(R)       m                 6         np             2      2
FIMUL                m                 6         np             2      2
FIDIV(R)             m          22/36/42 p)      np            38 o)   2
FICOM                m                 4         np             0      0
FTST                                   1         np             0      0
FXAM                               17-21         np             4      0
FPREM                              16-64         np             2      2
FPREM1                             20-70         np             2      2
FRNDINT                             9-20         np             0      0
FSCALE                             20-32         np             5      0
FXTRACT                            12-66         np             0      0

FSQRT                                 70         np            69 o)   2
FSIN FCOS                         16-126         np             2      2
FSINCOS                           17-137         np             2      2
F2XM1                              13-57         np             2      2
FYL2X                             22-111         np             2      2
FYL2XP1                           22-103         np             2      2
FPATAN                            19-134         np             2      2
FPTAN                             17-173         np            36 o)   0

FNOP                                   2         np             0      0
FXCH                 r                 1         np             0      0
FINCSTP FDECSTP                        2         np             0      0
FFREE                r                 2         np             0      0
FNCLEX                               6-9         np             0      0
FNINIT                             12-22         np             0      0
FNSAVE               m           124-300         np             0      0
FRSTOR               m             70-95         np             0      0
WAIT                                   1         np             0      0
-----------------------------------------------------------------------------
注:
m) 格納する値は1クロック前に必要。
n) オーバーラップする命令もFMULのときは1。
o) 整数乗算命令とオーバーラップできない。
p) FDIVは24,53,64ビット精度でそれぞれ、19,33,39クロックサイクルかかる。FIDIVは、もう3クロックかかる。精度は浮動小数点制御ワードのビット8〜9で定義される。
q) 最初の4クロックサイクルは、先行する整数命令とオーバーラップする。段落17.12参照。

22. MMX命令(PMMX)

MMX命令のタイミングのリストは不要である。MMX乗算命令が3クロックかかる以外はすべて1クロックサイクルだからである。MMX乗算命令はオーバーラップしてパイプライン動作可能で、クロックサイクルあたり1乗算のスループットが得られる。

EMMS命令は1クロックサイクルしかかからないが、EMMSの後の最初の浮動小数点命令は約58クロック余分にかかるし、浮動小数点命令の後の最初のMMX命令は約38クロック余分にかかる。PMMXでは、EMMS命令の後のMMX命令にはペナルティーはないが、PIIではペナルティーの可能性がある。

MMX演算ユニットはパイプライン中でロードユニットの1ステップ後にあるので、MMX命令でメモリオペランドを使うことにペナルティーはない。しかし、MMXレジスタからメモリまたは32ビットレジスタにデータを格納するときはペナルティーがある。データは1クロック先立って準備されていなければならない。これは浮動小数点ストア命令と同様である。

EMMSを除くすべてのMMX命令は、どちらのパイプでもペアにできる。MMX命令のペアリング規則は段落8.10に書かれている。


23. コードの速度のテスト

Pentiumファミリ・プロセッサは、64ビットの内部カウンタを持ち、RDTSC(read time stamp counter)命令を使ってEDX:EAXに読み込むことができる。これはコード片が正確に何クロックサイクルかかるか調べるのにたいへん便利である。

以下のプログラムはコード片の実行にかかるクロックサイクル数を測るのに便利である。プログラムはテストされるコードを10回実行し、10個のクロックカウントを格納する。PPlainとPMMXでは、プログラムは16ビットと32ビットの両方のモードで使用可能である。

ITER    EQU     10              ; 繰り返し回数
OVERHEAD EQU    15              ; PPlainは15、PMMXは17

RDTSC   MACRO                   ; RDTSC命令を定義
        DB      0FH,31H
ENDM

.DATA                           ; データセグメント
ALIGN   4
COUNTER DD      0               ; ループカウンタ
TICS    DD      0               ; クロックの一時格納場所
RESULTLIST  DD  ITER DUP (0)    ; テスト結果のリスト

.CODE                           ; コードセグメント
BEGIN:  MOV     [COUNTER],0     ; ループカウンタをリセット
TESTLOOP:                       ; テストループ
;****************   ここでいろんな初期化をする:     ************************
        FINIT
;****************   初期化終わり                    ************************
        RDTSC                   ; クロックカウンタを読む
        MOV     [TICS],EAX      ; カウンタを保存
        CLD                     ; ペアにできない詰めもの
REPT    8
        NOP                     ; 影落とし効果を避けるための、8つのNOP
ENDM

;****************   ここにテストしたい命令を置く:   ************************
        FLDPI                   ; これはただの例
        FSQRT
        RCR     EBX,10
        FSTP    ST
;********************* テストしたい命令の終わり     ************************

        CLC                     ; 影を持つ、ペアにできない詰めもの
        RDTSC                   ; 再びカウンタを読む
        SUB     EAX,[TICS]      ; 差を計算
        SUB     EAX,OVERHEAD    ; 詰めものなどに使われるクロックを引く
        MOV     EDX,[COUNTER]   ; ループカウンタ
        MOV     [RESULTLIST][EDX],EAX   ; 結果を表に格納
        ADD     EDX,TYPE RESULTLIST     ; カウンタを増やす
        MOV     [COUNTER],EDX           ; カウンタを格納
        CMP     EDX,ITER * (TYPE RESULTLIST)
        JB      TESTLOOP                ; ITER回繰り返し

; RESULTLISTにはいっている値を読み出すコードをここに挿入

テストされるコード片の前後にある「詰めもの」の命令は、PPlainで整合性のある結果を得るために入れてある。CLDは、初回と引き続く回でペアリングが必ず同じになるようにするために挿入された、ペアにできない命令である。PPlainで、先行する命令の影に隠れて、テストされるコードのどんなプリフィックスもデコードされないように、8つのNOPが挿入されている。ここでは1バイト命令を使って、初回と引き続く回で同じペアリングが得られるようにした。テストされるコードの後のCLCは、RDTSCの0FHプリフィックスをデコードできる影を持つ、ペアにできない命令で、このためプリフィックスのデコードは、PPlainでテストされるコードの影落とし効果に依存しない。

PMMXでは、FIFO命令バッファを空にしたいならテストする命令の前に XOR EAX,EAX / CPUID を挿入するとよいし、FIFOバッファをいっぱいにしたいなら、何か時間のかかる命令(例えばCLIやAAD)を挿入するとよい。

PProとPIIでは、RDTSCが他と並列に実行されないように、CPUIDのような直列化する命令を各RDTSCの前後に置かなければならない。(CPUIDは直列化命令である。つまり、パイプラインをフラッシュして、進む前にペンディングしている操作がすべて完了するのを待つ。これはテストの目的には便利である。CPUIDは引き続く命令のプリフィックスをデコードできる影を持たない。)

RDTSC命令はPPlainとPMMXの仮想モードでは実行できないので、DOSのプログラムを走らせているなら、リアルモードにしなければならない。(ブート中にF8を押して、「Safe モード(コマンドプロンプトのみ)」または、「bypass startup files」を選ぶ)。

Pentiumプロセッサは特別な性能モニタ用のカウンタを持っており、キャッシュミス、ミスアラインメント、AGIストールなどのようなできごとを数えることができる。性能モニタ用カウンタの使い方についてはこのマニュアルではカバーしないが、MMXテクノロジ・デベロッパーズ・マニュアルに書かれている。


24. Pentium ProとPentium IIプロセッサ

Pentium ProとPentium IIプロセッサは、以前のものとはたいそう異なる。各命令はマイクロオペレーション(uop)にデコードされ、実行前に並べ替えられる。このことの主な利点は、プロセッサが最適化の大部分―複雑な命令を単純な命令に分割、独立な操作が並列に実行できるように、命令を並べ替え―をあなたに代わってやってくれることである。これらの動的プロセッサは、従って、最適化されていないコードでは以前よりうまく実行できる。もう一つの利点は、レジスタリネーミングである。
例:
        MOV     EAX,[ESI]
        INC     EAX
        MOV     [EDI],EAX
        MOV     EAX,[ESI+4]
        INC     EAX
        MOV     [EDI+4],EAX
この場合、動的プロセッサは、二つの計算が並列に実行できるように、後半の3命令に異なる物理レジスタを使用し、EAXにリネームするのである。[ESI]がキャッシュミスし、[ESI+]がキャッシュヒットした場合、後半の計算が先に完了する。論理レジスタ名より多くの物理レジスタがあるので、レジスタが足りないときには、この自動レジスタリネーミングを利用できる。

動的プロセッサにはしかし、欠点もある。最も重要なのは、予測ミスしたジャンプは非常に高価(10〜20クロックサイクル!)であることである。この問題を補償するため、動的プロセッサは、ある状況のもとで条件ジャンプの代わりに使える、条件つき移動命令を持っている。

もう一つの欠点は、部分レジスタストールと部分メモリストールである。レジスタの一部に書いた後でレジスタ全体を使うことは、PProとPIIに重大な遅れを生む。

例:
MOV AL,[EBX] / MOV ECX,EAX
このペナルティーは MOV ZX EAX,BYTE PTR [EBX] を使うか、最初にレジスタ全体をゼロにすることで避けられる。
XOR EAX,EAX / MOV AL,[EBX] / MOV ECX,EAX
動的プロセッサは、レジスタが自分自身とXORされるという組み合わせを認識して、部分レジスタストールを避けるように特別に設計されている。これはPentiumとPentiumProプロセッサの両方で動作するコードを書くのに望ましい方法である。プロセッサは、次の予測ミスした分岐か、割り込みまで、レジスタが自分自身とXORされたことを覚えている。

プロセッサが、異なる供給元からのデータをレジスタの異なる部分に入れて合わせることが必要なときにはいつでも、部分レジスタストールが起きる。

例:
        MOV     AL, [mem1]
        MOV     AH, [mem2]
        MOV     [mem3], AX
これは次のように変更するべき:
        MOVZX   EAX, BYTE PTR [mem1]
        MOVZX   EBX, BYTE PTR [mem2]
        SHL     EBX, 8
        OR      EAX, EBX
        MOV     [mem3], AX

プロセッサが二つの異なる供給元からのビットを一つのレジスタに合わせなければならないときはいつでも、部分レジスタストールが発生する。これはフラグレジスタでも起きる。

        SAHF
        JL      XX
SAHFはフラグビットのいくつかに書き込むが、オーバーフローフラグには書き込まない。JL命令はサインフラグとオーバーフローフラグの両方をよむので、SAHFのサインフラグと、SAHFの前のオーバーフローフラグを合わせなければならない。これは部分レジスタストールを与える。同じことは、ADDのようなフラグを変更する命令の後にPUSHFまたはPUSHFDが続くときにも起きる。PUSHFDはフラグレジスタの32ビットすべてをよむが、ADD命令はこれらのうち6つにしか書き込まないので、ここでも部分ストールが起きる。

同じメモリを異なるデータサイズで番地づけるときには、メモリ操作も部分ストールを起こす。

例:
MOV [ESI],AL  /  MOV EBX,[ESI]
この状況は、部分レジスタストールと同じである: 最初の命令がESIの指すDWORDの最初のバイトに書き、2番目の命令が4バイト読む。DWORDをEBXに読み込むためには、ALからのバイトをメモリからの残り3バイトと合わせなければならない。

これは部分メモリストールを与えない:

MOV [ESI],EAX  /  MOV BL,[ESI]
しかしこれは与える:
MOV [ESI],EAX  /  MOV BL,[ESI+1]
この状況は部分レジスタストールと似ていない。ここでの規則は、書き込んだものの一部を読むことは、アラインメントが同じでないときに部分ストールを起こすということである。

3番目の種類のストールは、CLでシフトまたはローテートした後どのフラグビットを調べても起きる。

これら3種類のストールは、間に別の命令をはさむことでマスクできる。 この文書で述べている最適化の大部分は、インテル以外のプロセッサを含め、他のプロセッサに対して負の影響は少しだけ、あるいは全くないが、知っておくべき問題がいくつかある。

レジスタの一部に書き込んだ後でレジスタ全体を使うことは、80486では中程度の遅れを生み、PentiumProとPentiumIIでは重大な遅れを生む。これは部分レジスタストールと呼ばれる。

例:
MOV AL,[EBX] / MOV ECX,EAX
このペナルティーは、レジスタ全体をゼロにすることで避けられる。
XOR EAX,EAX / MOV AL,[EBX] / MOV ECX,EAX
または、MOVZX EAX,BYTE PTR [EBX] を使う。 同様のペナルティーは、同じメモリを異なるデータサイズで番地づけるときにも起きる。
例:
MOV [ESI],EAX / MOV BL,[ESI]

浮動小数点命令のPentium向けスケジューリングは、しばしばたくさんの余計なFXCH命令を要する。これは古いマイクロプロセッサの実行を遅くするが、PentiumProやインテル以外の進んだプロセッサではそうではない。

PentiumMMXとPentiumIIプロセッサで使えるMMX命令は、超並列の整数計算にたいへん便利である。

PentiumProの最も有利な点は、最適化の多く―命令の並べ変えや、複雑な命令を単純な命令に分割すること―をあなたに代わってやってくれることである。しかし、完全に最適化されたコードでは、二つのプロセッサの違いは小さくなる。

二つのプロセッサは、基本的に同じ数の実行ユニットを持つため、スループットは同じに近いはずである。PProはメモリの読み書きのための分離されたユニットを持ち、三つの操作を、そのうち一つがメモリ読み込みなら、同時に行えるが、一方、Pentiumのように二つのメモリ読み込みや書き込みを同時に実行することは、できない。

PProは以下の点でPentiumよりよい。

PProは次の点でPentiumに劣る。

この結果Pentium Proは、完全に最適化されていて、多くの予測できない分岐を持ち、自然な並列性のほとんどあるいは全くない浮動小数点コードがたくさんあるコードでは、実はPentiumより遅いかもしれない。他の場合はたいてい、PProのほうが速い。

どちらのプロセッサの欠点もたいてい、注意深い最適化と32ビットフラットモードの使用で回避できる。しかし、PProに置ける予測ミスしたジャンプの問題は、条件つきMOV命令を代わりに使える場合以外は、避けることができない。

コードに以前のプロセッサでの互換性を持たせたいなら、MMXおよびPentiumProプロセッサの新しい命令を利用することは、問題を引き起こすことになる。解決策は、コードのいくつかのバージョンを、それぞれ特定のプロセッサ向けに最適化して、書くことであろう。プログラムは自動的にどのプロセッサが走っているか検出し、適当なバージョンのコードを選ぶべきである。このような複雑なアプローチはもちろん、プログラムの最も決定的な部分にだけ必要である。