How to optimize for the Pentium
family of the microprocessors (In Japanese)

Original (in English): http://www.agner.org/assem/
Copyright (c) 1996, 2000 by Agner Fog. Last modified 2000-03-31.
Translation into Japanese by Nobuhisa Fujinami and Takashi Itoh. Last modified 2000-05-30.

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

注意: このページは、現在誤訳の訂正を行っています。それとは無関係に、正確な内容については常に原文を参照してください。

目次

  1. はじめに
  2. 文献
  3. 高級言語からアセンブリ言語の関数を呼ぶには
  4. デバッグと確認
  5. メモリモデル
  6. アラインメント
  7. キャッシュ
  8. 初めての実行と繰り返し実行の比較
  9. 番地生成インターロック(PPlain and PMMX)
  10. 整数命令のペアリング
    1. 完全なペアリング
    2. 不完全なペアリング
  11. 複雑な命令を単純な命令に分割(PPlain and PMMX)
  12. プリフィクス(PPlain and PMMX)
  13. パイプラインの概要(PPro, PII and PIII)
  14. 命令のデコード(PPro, PII and PIII)
  15. 命令の取り込み(PPro, PII and PIII)
  16. レジスタ・リネーミング(PPro, PII and PIII)
    1. 依存関係の解消
    2. レジスタ・リード・ストール
  17. アウト・オプ・オーダー実行(PPro, PII and PIII)
  18. リタイアメント(PPro, PII and PIII)
  19. パーシャル・ストール(PPro, PII and PIII)
    1. パーシャル・レジスタ・ストール
    2. パーシャル・フラグ・ストール
    3. シフト命令・回転命令の後のフラグ・ストール
    4. パーシャル・メモリ・ストール
  20. 依存の連鎖(PPro, PII and PIII)
  21. ボトルネックを探す
  22. ジャンプと分岐(全てのプロセッサ)
    1. 分岐予測(PPlain)
    2. 分岐予測(PMMX, PPro, PII and PIII)
    3. ジャンプの回避(全てのプロセッサ)
    4. フラグを用いた条件分岐の回避(全てのプロセッサ)
    5. 条件分岐を条件移動命令で置き換える(PPro, PII and PIII)
  23. コードサイズの縮小(全てのプロセッサ)
  24. 浮動小数点コードのスケジューリング(PPlain and PMMX)
  25. ループの最適化(全てのプロセッサ)
    1. ループの最適化(PPlain and PMMX)
    2. ループの最適化(PPro, PII and PIII)
  26. 問題となりやすい命令
    1. XCHG命令(全てのプロセッサ)
    2. キャリーフラグを通した回転命令(全てのプロセッサ)
    3. ストリング命令(全てのプロセッサ)
    4. ビットテスト命令(全てのプロセッサ)
    5. 整数乗算命令(全てのプロセッサ)
    6. WAIT命令(全てのプロセッサ)
    7. FCOM命令+FSTSW AX命令(全てのプロセッサ)
    8. FPREM命令(全てのプロセッサ)
    9. FRNDINT命令(全てのプロセッサ)
    10. FSCALE命令(全てのプロセッサ)
    11. FPTAN命令(全てのプロセッサ)
    12. FSQRT命令(PIII)
    13. MOV [MEM], ACUUM命令(PPlain and PMMX)
    14. TEST命令(PPlain and PMMX)
    15. ビットスキャン命令(PPlain and PMMX)
    16. FLDCW命令(PPro, PII and PIII)
  27. 特別な話題
    1. LEA命令(全てのプロセッサ)
    2. 除算命令(全てのプロセッサ)
    3. 浮動小数点レジスタの解放(全てのプロセッサ)
    4. 浮動小数点からMMX命令への移行(PMMX, PII and PIII)
    5. 浮動小数点の整数への変換(全てのプロセッサ)
    6. 整数命令を使った浮動小数点演算(全てのプロセッサ)
    7. 浮動小数点命令を使った整数演算(PPlain and PMMX)
    8. データブロックの移動(全てのプロセッサ)
    9. 自己改変コード(全てのプロセッサ)
    10. プロセッサの見分け方(全てのプロセッサ)
  28. 命令タイミング(PPlain and PMMX)
    1. 整数演算命令
    2. 浮動小数点演算命令
    3. MMX命令(PMMX)
  29. 命令タイミングとμ-OPSへの分解(PPro, PII and PIII)
    1. 整数演算命令
    2. 浮動小数点演算命令
    3. MMX命令(PII and PIII)
    4. XMM命令(PIII)
  30. コードの速度のテスト
  31. いろいろなマイクロプロセッサの比較


1. はじめに

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

このマニュアルの情報は、私自身の調査と試験に基づいており、さまざまな人たちから受け取った情報で補足されている。このマニュアルのために追加情報を私に送ってくれた人々に感謝したい。このマニュアルは他の情報源に比べて理解しやすく正確で、他に見られない細かい事項をたくさん含んでいる。この情報を使えば、あなたは多くの場合に、あるコード片が正確に何クロックサイクルかかるのか計算することができるようになる。私はこのマニュアルに含まれている情報のすべてが正しいとは主張しない。いくつかのタイミング等は正確に測定することが困難あるいは不可能で、また私には、インテルのマニュアルの著者が持っているような技術的な内部情報を見る手段がない。

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

    略称            名前
    ------------------------------------------------
    PPlain          plain old Pentium (without MMX)
    PMMX            Pentium with MMX
    PPro            Pentium Pro
    PII             Pentium II(CeleronとXeonを含む)
    PIII            Pentium III(変種を含む)
    ------------------------------------------------

アセンブリ言語の文法はMASM5.10に従っている。公式なX86のアセンブリ言語は存在しないが、大部分のアセンブラがMASM5.10互換モードを持っているため、あなたが手にすることのできる事実上の標準に最も近い。しかしながら、私はMASM5.10の使用をお勧めしない。というのは、32ビットモードにおいて深刻なバグがあるからである。TASMか若しくはより新しいバージョンのMASMをお勧めする。

このマニュアルの中のいくつかの注釈はインテルに対する批判と映るかもしれない。しかし、他のメーカーの方が優れているという意味に取らないでいただきたい。ペンティアム・マイクロプロセッサ・ファミリーは、おそらくどの競争メーカーよりも速いと思われ、またそのように証明されており、良い試験結果がある。こういった理由により、他の競争メーカーについては、私や他のいかなる人の似たような独自の調査もされたことはない。

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

PPlain、PMMXチップのハードウェア設計は、一般的な最適化方法を使ったというよりはむしろ、いくつかのよく使われる命令やその組合せに特に最適化された、多くの特徴を持っている。その結果、ソフトウェアをこの設計向きに最適化するのはかなり複雑で、多くの例外があるが、相当な性能向上が可能かもしれない。PPro、PII、PIIIプロセッサはたいそう異なった設計となっており、プロセッサは命令をアウト・オブ・オーダー実行することにより、かなりの最適化作業の面倒をみている。しかし、これらのプロセッサのより複雑な設計は多くの潜在的なボトルネックを作り出しており、これらのプロセッサ向けに手で最適化することにより多くの利益があるかもしれない。

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

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

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

高級言語のコンパイラのいくつかは特定のプロセッサ向けの比較的よい最適化を提供しているが、手でさらに最適化することは、普通もっと良い性能を生み出すことができる。

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

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


2. 文献

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

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

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

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

VTUNEはコードを最適化するためにインテルから出ているツールである。私はこのツールをテストしたことはないので、ここではいかなる評価も与えることはできない。

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

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


3. 高級言語からアセンブリ言語の関数を呼ぶには

インラインアセンブラを用いる方法と、サブルーチンを完全にアセンブラで書き、あなたのプロジェクト中でリンクする方法がある。後者を選んだ場合は、高水準言語を直接アセンブリ言語に翻訳できるコンパイラの使用をお勧めする。関数コールの方法が正しく得られることが確実になる。これは大部分のC++コンパイラで可能である。

関数の呼び出し方法と名前の変換はたいそう複雑である。多数の異なる呼び出し方法があり、コンパイラのブランドが異なればこの点で互換性がない。アセンブリ言語のサブルーチンをC++から呼び出そうとしているなら、整合性と互換性の点から最もよい方法は、関数を extern "C" と _cdecl で宣言することである。アセンブリ言語のコードは、アンダースコア(_)が先頭についた関数名を持ち、外部名の大文字小文字を区別する(オプション -mx)ようにアセンブルされるはずである。

オーバーロード関数や、オーバーロード演算子、メンバ関数その他のC++特有の機能を使うには、まずC++でコーディングし、正しいリンク情報と呼び出し規約を得るためにC++ソースをアセンブリ言語に翻訳する必要がある。細部については、各々のメーカーのコンパイラ毎に異なる。アセンブリ関数を extern "C" と _cdecl で宣言することなしに異なったコンパイラで呼び出し可能にするには、各々のコンパイラ毎に外部参照名を与える必要がある。例えば、オーバーロードされた square 関数を次に示す。

  ; int square (int x);
  SQUARE_I PROC NEAR             ; 整数2乗関数
  @square$qi LABEL NEAR          ; Borland コンパイラのためのリンク名
  ?square@@YAHH@Z LABEL NEAR     ; Microsoft コンパイラのためのリンク名
  _square__Fi LABEL NEAR         ; Gnu コンパイラのためのリンク名
  PUBLIC @square$qi, ?square@@YAHH@Z, _square__Fi
          MOV     EAX, [ESP+4]
          IMUL    EAX
          RET
  SQUARE_I ENDP

  ; double square (double x);
  SQUARE_D PROC NEAR             ; 倍精度浮動小数点2乗関数
  @square$qd LABEL NEAR          ; Borland コンパイラのためのリンク名
  ?square@@YANN@Z LABEL NEAR     ; Microsoft コンパイラのためのリンク名
  _square__Fd LABEL NEAR         ; Gnu コンパイラのためのリンク名
  PUBLIC @square$qd, ?square@@YANN@Z, _square__Fd
          FLD     QWORD PTR [ESP+4]
          FMUL    ST(0), ST(0)
          RET
  SQUARE_D ENDP

パラメータの渡し方は呼び出し規約に依存する。

    呼び出し規約   スタック上のパラメータの順序  パラメータの消去
      _cdecl    最初のパラメータは下位アドレス    呼び出し側
      _stdcall   最初のパラメータは下位アドレス    サブルーチン
      _fastcall  コンパイラによる           サブルーチン
      _pascal   最初のパラメータは上位アドレス    サブルーチン
16ビットモードのDOSとWindows、CとC++におけるレジスタの用途
16ビットの戻り値はAXレジスタ、32ビットの戻り値はDX:AX、浮動小数点の戻り値はST(0)である。レジスタAX, BX, CX, DX, ESと算術フラグは手続きによって破壊されるかもしれない。すなわちすべての他のレジスタはどこかに保存し、しかる後に復元しなければならない。手続きはレジスタSI, DI, BP, DSに頼ることもあり、SSは他の手続きを呼んでも変化しない。

32ビットモードのWindows、C++または他の言語の場合
整数の戻り値はEAXレジスタ、浮動小数点の戻り値はST(0)である。レジスタEAX, ECX, EDX(EBXは除く)は手続きによって破壊されるかもしれない。すなわちすべての他のレジスタはどこかに保存し、しかる後に復元しなければならない。セグメントレジスタは一時的にも破壊してはならない。フラットセグメントを指すすべてのCS, DS, ESとSSがそうである。FSはオペレーティング・システムによって使われる。GSは使われないが、予約されている。フラグは以下の制約下で変化する可能性がある。方向フラグは初期値0である。方向フラグは一時的にセットされるかもしれないが、いかなる呼び出しまたは戻る前にも、クリアされていなければならない。浮動小数点レジスタ・スタックは手続きの始まりにおいて空でなければならない。但しST(0)が戻り値として用いられる場合を除く。MMXレジスタは手続きによって破壊される可能性があり、浮動小数点レジスタを用いるかもしれないすべての手続きから戻る前と呼び出す前に、EMMSによって同様にクリアされるかもしれない。XMMレジスタ・パラメータの渡し方と戻り値はインテルのアプリケーションノートの589ページに書かれている。手続きはレジスタEBX, ESI, EDI, EBPに頼ることもあり、すべてのセグメントレジスタは他の手続きを呼んでも変化しない。


4. デバッグと確認

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

高級言語で書かれたテストプログラムのサブルーチンが動くようになったら、アセンブリ言語に翻訳する準備ができたことになる。

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

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

プログラムの最も決定的な部分の速度を30章で述べる方法でテストせよ。コードが期待に比べてはっきり遅ければ、最もありそうな理由は、キャッシュミス(6章)、オペランドのミスアラインメント(5章)、最初の実行のペナルティ(8章)、分岐予測ミス(22章)、命令取り込みミス(15章)、レジスタ・リード・ストール(16章)、または長い依存の連鎖(20章)である。

高度に最適化されたコードは他人にとって非常に読みにくく、理解しにくくなりがちであり、たとえあなたであってもしばらく後に読み返せば同じことである。コードの維持ができるように、手続きやマクロのような小さな論理ユニットに分割することは重要である。これらはよく定義されたインターフェースと適当な注釈が必要である。コードが読みにくくなればなるほど、良い資料がより重要になる。


5. メモリモデル

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


6. アラインメント

RAM上のすべてのデータは下のように2、4、8、または16で割り切れる番地にアラインするべきである。
                      アラインメント     アラインメント
    オペランドサイズ   PPlainとPMMX     PPro、PIIとPIII
    ------------------------------------------------------
    1  (byte)              1                   1
    2  (word)              2                   2
    4  (dword)             4                   4
    6  (fword)             4                   8
    8  (qword)             8                   8
    10 (tbyte)             8                  16
    16 (oword)         利用不可               16
    ------------------------------------------------------

PPlainとPMMAXにおいては、ミスアラインされたデータのアクセスは最低3クロックサイクル余計にかかる。キャッシュラインの境界をまたぐと、ペナルティはさらに高くなる。

PPro, PIIとPIIIにおいては、ミスアラインされたデータがキャッシュライン境界をまたぐ時、6-12クロック余計にかかる。16バイトよりも小さいミスアラインされたオペランドでも32バイト境界をまたがなければ、ペナルティはない。

8または16バイトでアラインされたDWORDのスタックは、問題となることがある。よくある方法は、アラインされたフレームポインタを用意することである。アラインされたローカルデータを持つ関数は次のようなものであろう。

_FuncWithAlign PROC NEAR
        PUSH    EBP                        ; 始まりのコード
        MOV     EBP, ESP
        AND     EBP, -8                    ; フレームポインタを8バイトでアラインする
        FLD     DWORD PTR [ESP+8]          ; 関数パラメータ
        SUB     ESP, LocalSpace + 4        ; ローカル領域の確保
        FSTP    QWORD PTR [EBP-LocalSpace] ; アラインされた領域に何か書き込む
        ...
        ADD     ESP, LocalSpace + 4        ; 終わりのコード ESPの復元
        POP     EBP                        ; (PPlain/PMMXでAGIストールが起きる)
        RET
_FuncWithAlign ENDP

アラインされたデータはいつも重要とは言え、PPlainとPMMXではコードのアラインは不要である。PPro、PII、PIIIにおいて、コードをアラインする原理は、(15章)で説明している。


7. キャッシュ

PPlainとPProはコード用に8KB、データ用に8KBのオンチップキャッシュ(一次キャッシュ)を持っている。PMMX、PIIとPIIIはコード用に16KB、データ用に16KB持っている。一次キャッシュにあるデータはちょうど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とPIIIは四つある。

この結果、キャッシュは、番地のビット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クロックサイクルしかかからない(初回を除いて)。たいへん考慮に値する改良である!

既に述べたように、PMMX, PIIとPIIIは同じセット値を持つ四つのキャッシュラインを持てるように、四ウェイのキャッシュを備えている(あるインテルの説明書は誤ってPIIのキャッシュは二ウェイだと述べている)。

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

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

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

メモリから大きなデータブロックを読む時、その速度はキャッシュラインを満たす時間によって制限を受ける。データを非連続的に読むことにより、速度を改善することができる場合がある。すなわち、一つのキャッシュラインからデータを読み終える前に、次のキャッシャラインの最初の要素を読み始める場合である。この方法はPPlainとPMMXではメモリと二次キャッシュから読む場合、PPro、PIIとPIIIでは二次キャッシュから読む場合に、20%〜40%速度を上げることができる。この方法の欠点はもちろん、プログラムが汚く複雑になることである。この技巧についての更なる情報はwww.intelligentfirm.comをご覧いただきたい。

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

PPro、PIIとPIIIでは、書き込みミスでは通常、キャッシュラインをロードする。しかし、メモリの領域が違う振る舞いをするように設定しておくこともできる。例えば、VRAMのように。(Pentium Pro ファミリ ディベロッパーズマニュアル 下巻 オペレーティング・システム ライターズマニュアルを参照のこと。)

メモリーの読み書きの速度を上げるこれ以外の方法については 27章8で後述する。

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

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

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

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

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


8. 初めての実行と繰り返し実行の比較

初めて実行されるコード片は、普通繰り返し実行されるよりも多くの時間がかかる。その理由は次の通りである。
  1. RAMからキャッシュへコードを読み込む時間は、実行時間よりも長い。
  2. 実行コードにより参照されるデータはキャッシュへ読み込まれなければならず、それには命令を実行するよりもはるかに多くの時間がかかるかもしれない。コードが繰り返し実行されれば、より似たようなデータがキャッシュに入る。
  3. ジャンプ命令は初めての実行の際にはBTBにないので、分岐予測はほとんど役に立たない。22章をご覧いただきたい。
  4. PPlainにおいては、コードの解釈がボトルネックである。命令長を決定するのに1クロックサイクルを要すると、クロックサイクル当たり2つの命令を解釈することは不可能である。と言うのは、プロセッサは次の命令がどこから始まるのかがわからないからである。PPlainはこの問題をキャッシュに残っている命令が実行されてから、その長さを記憶しておくことにより解決している。この結果として、命令の組は最初の実行時は、二つの命令の前者が1バイト長である場合を除き、ペアになって実行されない。PMMX, PPro, PIIとPIIIは最初の解釈時にこのペナルティを受けない。

これら四つの理由により、ループ中にあるコード片は一般的に、最初の実行時には、続く実行時より余計な時間がかかる。

もしコードキャッシュに収まりきらない大きなループがあると、キャッシュから実行されないため、毎回ペナルティを受ける。それゆえ、ループをキャッシュ内に収めるように試みるべきである。

ループ中に多くのジャンプ、呼び出し、分岐がある場合、BTBミスのペナルティが繰り返し起きる。

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


9. 番地生成インターロック(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とPIIIには、メモリー読み出しとLEA命令にはAGIストールはないが、メモリ書き込みにはAGIストールが存在する。 これは、連続したコードが書き込みの終了を待つのでない限り、さほど問題にならない。


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

10.1 完全なペアリング

PPlainとPMMXは、命令の実行のための二つのパイプライン、UパイプとVパイプを持つ。ある条件の元で、二つの命令を同時に、一つはUパイプで、もう一つはVパイプで実行できる。これはほとんど実行速度を倍にする。そのため、命令を並べ変えてペアにするのは有益である。 次の命令はどちらのパイプでもペアにできる。
次の命令はUパイプでのみペアにできる。 次の命令はどちらのパイプでも実行できるが、ペアにできるのはVパイプの時だけである。 他のすべての整数命令はUパイプでのみ実行可能であり、ペアにできない。

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

1. 最初の命令はUパイプでペアにでき、二番目の命令はVパイプでペアにできる。

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

3. 規則2でパーシャル・レジスタはレジスタ全体として扱われる。
例:

    MOV AL, BL  /  MOV AH, 0        ; 同じレジスタの異なる部分への書き込み
                                    ; ペアにできない

4. 規則2と3にかかわらず、フラグレジスタの一部に書き込む二つの命令はペアにできる。例:

    SHR EAX,4 / INC EBX             ; ペアOK

5. 規則2にかかわらず、フラグに書き込む命令と条件分岐はペアにできる。例:

    CMP EAX, 2 / JA LabelBigger     ; ペアOK

6. 次の命令の組合せは、両方がスタックポインタを変更するという事実にもかかわらず、ペアにできる。

    PUSH + PUSH,  PUSH + CALL,  POP + POP

7. プリフィックスつきの命令のペアリングには制限がある。プリフィックスにはいくつかの種類がある。

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

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

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バイトより長くなるかもしれないことで、それは、
12章で説明するように、1クロックサイクルで1命令しかデコードできないことを意味する。)

9. 両方の命令があらかじめロードされ、デコードされている。これは8章で説明されている。

10.PMMXのMMX命令には特別なペアリング規則がある。

10.2 不完全なペアリング

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

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

1. 二番目の命令がAGIストールを受ける場合(9章参照)。

2. 二つの命令はメモリの同じDWORDを同時にアクセスできない。次の例はESIが4で割り切れると仮定している。

     MOV AL, [ESI] / MOV BL, [ESI+1]
二つのオペランドは同じDWORD内にあるので、同時には実行できない。このペアは2クロックサイクルかかる。
     MOV AL, [ESI+3] / MOV BL, [ESI+4]
ここでは二つのオペランドはDWORD境界の両側にあるので、完全にペアになれ、1クロックサイクルしかかからない。

3. 規則2は二つの番地のビット2〜4が同じである場合に拡張される(キャッシュバンク競合)。DWORDの番地に対しては、これは二つの番地の差が32で割り切れてはならないことを意味する。 例:

     MOV [ESI], EAX / MOV [ESI+32000], EBX ;  不完全なペアリング
     MOV [ESI], EAX / MOV [ESI+32004], EBX ;  完全なペアリング

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

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

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

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クロックサイクル

5. ペアになった二つの命令が両方とも、キャッシュミス、ミスアラインメント、または分岐予測ミスによって余分な時間がかかるとき、そのペアは各命令単独よりは時間がかかるが、二つの和よりは少ない。

6. ペアにできる浮動小数点命令にFXCH命令が続いているものは、その次の命令が浮動小数点命令でなければ、不完全なペアリングとなる。

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

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

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クロックしかかからない。

次の例は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とPIIIでは、分割された命令がより小さなμ-OPSを生成しない限り必要ない。


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

一つまたは複数のプリフィックスを持つ命令は、Vパイプで実行できないかもしれず(
10章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のデコードの遅れはない)。

PPro、PIIとPIIIのプリフィックスのペナルティは14章に述べてある。


13. PPro, PIIとPIIIのパイプラインの概要

PPro, PIIとPIIIマイクロプロセッサのアーキテクチャは、インテルから出ている様々な説明書や指導書によって十分に説明されている。これらのマイクロプロセッサの働きを理解するために、これらの文献を紐解くことをお勧めする。私はコードの最適化に重要な部分に特に焦点を当て、構造を簡単に記述しようと思う。

命令コードはコードキャッシュから、アラインされた16バイトかたまりとして、16バイトのかたまり二つを保持できるダブルバッファに取り込まれる。命令はダブルバッファからデコーダへブロックとして移される。このブロックを ifetchブロック(instruction fetch block)と呼ぼうと思う。ifetchブロックは普通16バイト長だが、アラインされていない。ダブルバッファの用途は、16バイト境界(すなわち番地が16で割り切れる)をまたぐ命令のデコードを可能にすることである。

ifetchブロックは命令長デコーダへ移され、これは各々の命令の始まりと終わりを決定する。次に命令デコーダへ移される。各々のクロックサイクル毎に三つの命令をデコードするために、三つのデコーダがある。同じクロックサイクル内でデコードされる最高三つまでの命令グループは、デコードグループと呼ばれる。

デコーダは命令をマイクロオペシーション、略してμ-OPSへと翻訳する。簡単な命令は一つのμ-OPSを生成するが、その一方でもっと複雑な命令はいくつかのμ-OPSを生成するかもしれない。例えば、命令 ADD EAX,[MEM] は二つのμ-OPSを生成する。一つはソースオペランドをメモリから読み、もう一つは加算を実行する。命令をμ-OPSへと分割する目的は、システム中の後の取り扱いをより効率的にするためである。

三つのデコーダはD0、D1、そしてD2と呼ばれる。D0はすべての命令を扱うことができる一方、D1とD2は一つのμ-OPSを生成する簡単な命令のみ扱える。

デコーダから来たμ-OPSは、短いキューを通して、レジスタ・アロケーション・テーブル(RAT)へと移される。μ-OPSは後に常置レジスタ(EAX, EBXなど)に書かれることになるテンポラリレジスタ上で実行される。RATの目的は、μ-OPSにどのテンポラリレジスタを使うか知らせることと、レジスタ・リネーミング(後述)を可能にすることである。

RATの後、μ-OPSはリオーダ・バッファ(ROB)へと送られる。ROBの用途は、アウト・オブ・オーダー実行をすることにある。μ-OPSは、必要とするオペランドが利用可能となるまでリザベーション・ステーションに止まる。もし前に生成されたμ-OPSがまだ終了してないことが原因で一つのμ-OPSが遅らされると、ROBは時間を稼ぐために今実行できる他のμ-OPSを探すかもしれない。

実行の準備ができたμ-OPSは、実行ユニットへ送られ、五つのポートへ振り分けられる。ポート0と1は演算命令、ジャンプ等を扱うことができる。ポート2はメモリからの読み込みをすべて担当し、ポート3はメモリへの書き込みのための番地を生成し、ポート4はメモリへの書き込みを行う。

命令の実行が終了すると、ROB内でリタイアの準備ができた印が付けられる。そしてそれはリタイアメント・ステーションへと送られる。ここでμ-OPSによって使われたテンポラリレジスタは常置レジスタに書かれる。μ-OPSはアウト・オブ・オーダー実行をしてもよいが、それらは順序よくリタイアしなければならない。

次の章では、パイプラインの各々の段階でのスループットを最適化する方法の詳細について述べるつもりである。


14. 命令のデコード(PPro, PII and PIII)

私はここでは命令のデコードを、命令の取り込みよりも先に書こうと思う。というのは、命令の取り込みにおいて生ずる可能性かある遅延を理解するために、デコーダの働きを知る必要があるからである。

デコーダはクロックサイクル当たり三つの命令を扱うことができるが、それは特定の条件に合致した時に限られる。デコーダD0は1クロックサイクル当たり四つまでのμ-OPSを生成する命令を扱うことができる。デコーダD1とD2は一つのμ-OPSを生成する命令しか扱うことができず、それらの命令長が8バイトを超えてはならない。

同じクロックサイクルで二つあるいは三つの命令をデコードできる規則を簡単に要約してみよう。

DOにおいては、命令長に制限はない(インテルの説明書は別のことを言っているが)。但し三つの命令が16バイトのifetchブロックに収まっていなければならない。

生成されるμ-OPSの数が四つを超える命令のデコードには、2以上のクロックサイクルを必要とし、この間他の命令を並列にデコードすることはできない。

この規則に従うと、デコードグループの最初の命令が四つのμ-OPSを生成し、次の二つが各々一つのμ-OPSを生成したとすると、デコーダは1クロックサイクル当たり最大六つのμ-OPSを生成できることになる。最小は1クロックサイクル当たり二つのμ-OPSを生成する場合で、すべての命令が二つのμ-OPSを生成する時、D1とD2は全く使われない。

最大のスループットを得るために、命令を4-1-1型に従って並べることをお勧めする。二つ〜四つのμ-OPSを生成する命令の間には簡単な一つのμ-OPSを生成する命令を二つ、デコード時間を増やさないという意味においてはただで挿入できる。例えば

    MOV     EBX, [MEM1]     ; 1μ-OPS (D0)
    INC     EBX             ; 1μ-OPS (D1)
    ADD     EAX, [MEM2]     ; 2μ-OPS (D0)
    ADD     [MEM3], EAX     ; 4μ-OPS (D0)

この例ではデコードに3クロックサイクル要している。命令を並べ替え、二つのデコードクループに分けることにより、1クロックサイクル稼ぐことができる。

    ADD     EAX, [MEM2]     ; 2μ-OPS (D0)
    MOV     EBX, [MEM1]     ; 1μ-OPS (D1)
    INC     EBX             ; 1μ-OPS (D2)
    ADD     [MEM3], EAX     ; 4μ-OPS (D0)

今やデコーダは2クロックサイクルで八つのμ-OPSを生成し、それは多分満足のいくものである。その後のパイプライン内のステージでは1クロックサイクル当たり三つのμ-OPSしか扱うことができないので、デコード速度がそれより速ければ、デコードがボトルネックにならないと考えられる。しかしながら、次の章で述べるように命令取り込み機構の複雑さがデコードの足かせとなることがあるので、安全のために1クロックサイクル当たりのμ-OPSの生成速度が三つを超えるように狙いを定めたいと思うだろう。

各々の命令が生成するμ-OPSの数を表にしたものが29章にある。

命令のプリフィックスもデコーダにおいてペナルティを招く可能性がある。命令は数種類のプリフィックスを持つことができる。


15. 命令の取り込み (PPro, PII and PIII)

コードは、アラインされた16バイトのかたまりとしてコードキャッシュから取り込まれ、ダブル・バッファに置かれる。ダブルバッファと呼ぶのは、そのようなかたまり二つを保持できるからである。コードはダブル・バッファから取り出され、デコーダに普通16バイト長の、しかし、必ずしも16でアラインされていないブロックとして供給される。私はこれらのブロックをifetchプロック(instruction fetch blocks)と呼ぼうと思う。ifetchブロックがコードにおいて16バイト境界をまたいだ場合、ダブル・バッファの両方のブロックから取り出されなければならない。だから、ダブル・バッファの用途は、命令の取り込みが16バイト境界をまたいでもできるようにするためである。

ダブル・バッファは1クロックサイクル当たり16バイトのかたまり一つを取り込むことができ、1クロックサイクル当たり一つのifetchブロックを生成できる。ifetchブロックは普通16バイト長であるが、ブロック中に予測された分岐がある場合は、短くなり得る(22章を参照のこと)。

不幸なことに、ダブル・バッファは遅延なしにジャンプ命令の周囲のフェッチを扱うのに十分な大きさがない。ジャンプ命令を含むifetchブロックが16バイト境界をまたぐ場合、ダブル・バッファは二つのアラインされた16バイトのコードのかたまりを保持する必要がある。ジャンプ命令の後の最初の命令が16バイト境界をまたぐ場合、ダブル・バッファは有効なifetchブロックが生成できるようになるまでに二つの新しい16バイトのコードのかたまりを読まなければならない。これは、最悪の場合、ジャンプ命令の後の最初の命令が2クロックサイクル遅らされることを意味する。つまりジャンプ命令を含むifetchブロック中の16バイト境界のために1クロックサイクル、そしてジャンプ命令の後の最初の命令の16バイト境界のために1クロックサイクルのペナルティが課せられる。ジャンプ命令を含むifetchブロック中に二つ以上のデコードグループがある場合、賞与を得ることができる。というのは、ジャンプ命令の後で命令に先立ち一つまたは二つの16バイト単位のコードを取り込む余計な時間があるからである。この賞与は、後述する表に従ってペナルティを償えることがある。ジャンプ命令の後でダブル・バッファが16バイトのコードのかたまりを一つだけ取り込んだとすると、ジャンプ命令の後の最初のifetchブロックはこのかたまりと同一、つまり16バイト境界にアラインされる。言い換えれば、ジャンプ命令の後の最初のifetchブロックは最初の命令で始まるわけではなく、16で割り切れる直前の番地から始まる。ダブル・バッファが16バイトのコードのかたまりを二つ読み込める時間があれば、新しいifetchブロックは16バイト単位をまたぐことができ、ジャンプ命令の後の最初の命令から始まる。これらの規則を次のに要約した。

    ジャンプ命令  ifetchブロッ ジャンプ命令              ジャンプ命令
    を含むifetch  クに含まれる  の後の最初の  デコーダの  のあとの最初
    ブロックの数  16バイト境界  命令中の16バ  遅延        のifetchのア
                  の数          イト境界の数              ラインメント
    ------------------------------------------------------------------
          1             0             0            0           16
          1             0             1            1          命令
          1             1             0            1           16
          1             1             1            2          命令
          2             0             0            0          命令
          2             0             1            0          命令
          2             1             0            0           16
          2             1             1            1          命令
        3以上           0             0            0          命令
        3以上           0             1            0          命令
        3以上           1             0            0          命令
        3以上           1             1            0          命令

ジャンプ命令は命令取り込みを遅らせるので、ループは常に、その中に含まれる16バイト境界の数より少なくとも2クロックサイクル余計にかかる。

命令取り込み機構に伴う更なる問題は、新しいifetchブロックは、先のブロックが使い尽くされるまで生成されないことである。各々のifetchブロックはいくつかのデコードグループを含むことができる。16バイト長のifetchブロックが未完結の命令で終わっていると、次のifetchブロックはその命令で始まる。最初の命令は常にデコーダDOへ行き、次の二つの命令はもし可能であればD1とD2に行く。この結果、D1とD2が使われるのは最適より少なくなる。コードの構造が推薦できる4-1-1型をしていて、D1とD2に行くように意図されている命令がたまたまifetchブロックの最初の命令になったとすると、その命令はD0に行かなければならず、1クロックサイクルが無駄になる。これは多分ハードウェアの設計ミスである。少なくともこれは次善のデザインである。この問題の結果として、コード片のデコードにかかる時間は、最初のifetchブロックが始まる位置にかなり依存して変化する。

デコード速度が重要で、これらの問題を避けたいと思うならば、各々のifetchブロックがどこから始まるか知る必要がある。これは全く退屈な仕事である。最初にコードセグメントがパラグラフ(訳注: 16バイト)でアラインされるようにする必要がある。これは16バイト境界がどこにあるか知るために必要である。次にアセンブラからの出力リストを見て、命令長を知らなければならない(命令のコードのされ方を知り、それで命令長を予測する勉強をお勧めする)。一つのifetchブロックの始まる位置がわかれば、次のifetchブロックを次のように見つけることができる。ブロックを16バイト長にする。それが命令の境界で終われば、次のブロックはそこから始まる。命令で終わっていなければ、次のブロックはこの命令から始まる(ここでは命令長だけを数えればよい。いくつのμ-OPSが生成され、それらがどう働くかについては気にしなくてよい)。このようにしてコードのすべてを調べ、各々のifetchブロックの始まりに印をつけることができる。唯一の問題は、どこから始めるかを知ることである。一つのifetchブロックが始まる位置がわかれば、それに続くすべてのブロックがわかる。しかし最初のブロックがどこから始まるか知らなければならない。これからいくつかの指針を示す:

例が見たいと思っているに違いない:

 番地              命令              命令長    μ-OPS 予期されるデコーダ
 ----------------------------------------------------------------------
 1000h        MOV ECX, 1000             5         1       D0
 1005h   LL:  MOV [ESI], EAX            2         2       D0
 1007h        MOV [MEM], 0             10         2       D0
 1011h        LEA EBX, [EAX+200]        6         1       D1
 1017h        MOV BYTE PTR [ESI], 0     3         2       D0
 101Ah        BSR EDX, EAX              3         2       D0
 101Dh        MOV BYTE PTR [ESI+1],0    4         2       D0
 1021h        DEC ECX                   1         1       D1
 1022h        JNZ LL                    2         1       D2

最初のifetchブロックが1000H番地で始まり、1010H番地で終わると仮定しよう。これは MOV [MEM],0 の命令終わりの前なので、次のifetchブロックは1007H番地で始まり、1017H番地で終わるはずである。これは命令の境界なので、三番目のifetchブロックは1017Hで始まり、ループの残りをまかなうはずである。これをデコードするのにかかるクロックサイクル数はD0で解釈される命令数であり、LLのループでは繰り返し当たり5クロックサイクルである。最後の五つの命令をまかなうのは三つのデコード・ブロックを含むifetchブロックであり、16バイト境界上(1020H)に乗る。前記した表より、ジャンプ命令の後の最初のifetchブロックはジャンプ命令の後の最初の命令で始まり、それは1005H番地のラベルLLであり、1015H番地で終わる。これはLEA命令の前で終わるので、次のifetchブロックは1011H番地から1021H番地になり、残りのブロックで1021Hまでをまかなう。今LEA命令とDEC命令の両方がifetchブロックの始まりに降りてきて、D0に行かされる。D0でデコードされる命令は七つになったので、次の繰り返し時には7クロックサイクルかかる。最後のifetchブロックは一つのデコードグループ(DEC ECX / JNZ LL)だけを含み、これは16バイト境界にない。表に従えば、ジャンプ命令の後の次のifetchブロックは16バイト境界で始まり、これは1000H番地である。これは最初の繰り返しの状況と同じであり、つまりこのループのデコードには5クロックサイクルと7クロックサイクル交互にかかる。他のボトルネック原因がないので、完全なループ実行には1000回の繰り返しで6000クロックかかるはずである。開始番地が違えば、最初と最後の命令に16バイト境界が来て、その結果8000クロックかかるはずである。D1とD2の命令がifetchブロックの最初には一つも来ないように命令を並べ替えれば、5000クロックしかかからないようにできる。

上の例は慎重に計画すればコードの取り込みとデコードだけがボトルネックである。この問題を最も簡単に避ける方法は、クロックサイクル当たり四つ以上のμ-OPSを生成するように構成し、ここに記述したペナルティがボトルネックにならないようにすることである。小さなループではこれが不可能なことがあり、命令の取り込みとデコードを最適化するための方法を見つけ出さなくてはならない。

可能な一つの方法は、望ましくない16バイト境界を避けるため、手続きの開始番地を変えることである。境界が分かるように、コードセグメントをパラグラフでアラインしたことを思い出して欲しい。

ALIGN 16ディレクティブをループの入り口の前に挿入することにより、アセンブラはNOP命令と他の詰め物としての命令を入れ、最も近い16バイト境界まで埋めてくれる。XCHG EBX,EBXは2バイトの詰め物(いわゆる2バイトのNOP)である。誰が考えたのか知らないが、ほとんどのプロセッサでこれは二つのNOPよりも余計な時間がかかる! ループが長く実行されれば、ループの外側にあるいかなるものも速度にとって重要ではなく、準最適な詰め物命令を気にすることはない。しかし詰め物によってかかる時間が重要ならば、詰め物の命令を手で選んでもよい。その上、詰め物は何か役に立つ使い方をしてもよい。例えばレジスタ・リード・ストール(16章2節を参照のこと)を避けるためにレジスタをリフレッシュするなどがある。例として、EBPレジスタを番地指定に使うが滅多に更新しないならば、レジスタ・リード・ストールの可能性を減らすためにMOV EBP,EBPやADD EBP,0を詰め物として使用してよい。何も役に立たなくてもよいなら、FXCH ST(0)はよい詰め物である。というのはこの命令はどの実行ポートにも読み込みをしないからである。但し、ST(0)に有効な浮動小数点数が入っていなければならない。

他の治療法として、命令を、結果が影響を受けないように並べ替える方法がある。これは全く難しいパズルになりがちで、いつも満足のいく答えが見つかるとは限らない。

さらに他の可能性として、命令長を操作する方法がある。一つの命令を長さの異なる他の命令に置き換えられることがある。多くの命令が異なった長さの異なったバージョンでコードされる。アセンブラは可能な限りいつも最も短いバージョンの命令を選択する。しかししばしば、長いバージョンをハードコードできる。例えば、DEC ECXは1バイト長で、SUB ECX,1は3バイト長である。また、次のトリックを使って長い即値のバージョンで書くことができる。

         SUB ECX, 9999
         ORG $-4
         DD 1

メモリ・オペランドを伴った命令はSIBバイトで1バイト長くできる。しかし1バイト長い命令を作る最も簡単な方法はDS:セグメント・プリフィックス(DB 3EH)を付け加えることである。マイクロプロセッサは命令長が15バイトを超えない限り、冗長で無意味なプリフィックス(ロック・プリフィックスを除く)を受け入れる。メモリ・オペランドを含まない命令でさえ、セグメント・プリフィックスを持つことができる。だから、DEC ECXを2バイト長で書くことができる。

         DB  3Eh
         DEC ECX

命令が二つ以上のプリフィックスを持つと、デコーダでペナルティが課せられることを忘れてはならない。無意味なプリフィックスを持つ命令、特にリピート・プリフィックスとロック・プリフィックスは、命令コードにこれ以上空きがなくなったら、将来のプロセッサで使われる可能性がある。しかし、セグメント・プリフィックスはどの命令に使っても安全だと思う。

これらの方法により普通は、ifetch境界を望む所に置くことができる。それが退屈なパズルになりがちだとしても。


16. レジスタ・リネーミング (PPro, PII and PIII)

16.1 依存関係の解消

レジスタ・リネーミングはこれらのマイクロプロセッサによって使われる進歩した技術で、異なるコード部分の間の依存関係を取り除くために使われる。例:
         MOV EAX, [MEM1]
         IMUL EAX, 6
         MOV [MEM2], EAX
         MOV EAX, [MEM3]
         INC EAX
         MOV [MEM4], EAX

ここで、最初の三つの命令からはどんな結果も必要としないという意味において、最後の三つの命令は最初の三つの命令と独立しているので。初期のプロセッサで最適化するためには、最後の三つの命令にEAXと異なるレジスタを使用し、命令を並べ替えて、最後の三つの命令が最初の三つの命令と並列に実行されるようにしなければならなかっただろう。PPro、PIIとPIIIプロセッサではこれを自動的に行ってくれる。EAXに書き込む度に、新しいテンポラリレジスタが割り当てられる。この結果、MOV EAX,[MEM3]命令は先の命令に依存しなくなる。アウト・オブ・オーダー実行により、[MEM]への移動は、遅いIMUL命令より先に終わるだろう。

レジスタ・リネーミングは完全に自動的に行われる。命令がレジスタに書き込む度に、常置レジスタの代わりに新しいテンポラリレジスタが割り当てられる。レジスタからの読み出しと書き込みの両方を行う命令もリネーミングの原因となる。例えば、上のINC EAX命令は、一つのテンポラリレジスタを入力用に、もう一つのテンポラリレジスタを出力用に割り当てる。これはもちろん独立性を損なうことはない。しかし、後に説明するように、連続したレジスタの読み出しには、いくつかの重要な点がある。

すべての一般用レジスタ、即ちスタックポインタ、フラグ、浮動小数点レジスタ、MMXレジスタ、XMMレジスタとセグメントレジスタはリネームできる。コントロールレジスタと浮動小数点ステータスレジスタはリネームできない。これが、これらのレジスタの使用が遅い原因である。40個の万能テンポラリレジスタがあるので、すべてのテンポラリレジスタを使い切ってしまうことはありそうにない。

レジスタを0にする一般的な方法は、XOR EAX,EAXまたはSUB EAX,EAXである。これらの命令は先に入っていた値と独立とは見なされない。先にある遅い命令への依存を取り除くには、MOV EAX,0命令を使えばよい。

レジスタ・リネーミングはレジスタ・エイリアス・テーブル(RAT)とリオーダ・バッファ(ROB)によって制御される。デコーダからのμ-OPSはキューを通ってRATへ送られる。そしてROBとリザベーション・ステーションへと送られる。RATは1クロックサイクル当たり三つのμ-OPSを扱うことができる。これは、マイクロプロセッサのスループットは、平均して1クロックサイクル当たり三つのμ-OPSを超えられないということである。

リネーミングの数の実際的な制限はない。RATは1クロックサイクル当たり三つのレジスタをリネームでき、同じレジスタであっても1クロックサイクルに三回リネームできる。

16.2 レジスタ・リード・ストール

しかし、全く深刻な、別の制限がある。それは、1クロックサイクル当たり二つの常置レジスタからしか読み出すことができないということである。この制限は命令によって使われる全てのレジスタに適用される。但し、レジスタに書き込むのみの命令を除く。例:
         MOV [EDI + ESI], EAX
         MOV EBX, [ESP + EBP]

最初の命令は二つのμ-OPSを生成する。一つはEAXから読み出し、もう一つはEDXとESIから読み出す。次の命令は一つのμ-OPSを生成する。それはESPとEBPからの読み出す。EBXは命令が書き込むだけなので、読み出しには数えない。この三つのμ-OPSが一緒にRATに行ったと仮定しよう。私はRATに一緒に行く連続した三つのμ-OPSのグループに三つ組という言葉を使おうと思う。ROBが1クロックサイクル当たりに読み出せる常置レジスタは二つだけで、我々の三つ組は五つのレジスタ読み出しが必要なので、この三つ組がリザベーション・ステーションに来るまでに余計な2クロックサイクルぶん遅れるだろう。三つ組中で三つまたは四つのレジスタを読み出すならば、1クロックサイクル遅れるだろう。

同じ三つ組中で同じレジスタを、回数に数えられずに2回以上読み出すことができる。上の命令を次のように変えると、

         MOV [EDI + ESI], EDI
         MOV EBX, [EDI + EDI]
二つのレジスタを読み出すだけで済み(EDIとESI)、三つ組は遅延を受けない。

実行中断中のμ-OPSが書き込む予定のレジスタはROBに保存されているので、それが書き戻されるまで自由に読み出すことができる。書き戻しには少なくとも3クロックサイクル、普通はもっとかかる。書き戻しは最後の実行ステージで行われ、ここで値が利用可能となる。他の言い方をすれば、実行ユニットからその値がまだ出力できていないなら、RAT中ののレジスタはストールせずにいくつでも読み出せるということである。この理由は、値が利用可能になると直ちに、それを必要とするすべての後続のROBエントリに直接書き込まれるからである。しかし値が一旦テンポラリレジスタまたは常置レジスタに書き戻されると、その値を必要とする後続のμ-OPSがRATに行ったとき、値をレジスタ・ファイルから読み出さなければならない。そしてそれは二つの読み出しポートしか持っていない。RATから実行ユニットまでには三つのパイプライン・ステージがあり、一つの三つ組μ-OPS中で書かれるレジスタは、少なくとも次の3個の三つ組では自由に読み出すことができると確信してよい。書き戻しが、並べ替え、遅い命令、依存の連続、キャッシュミス、その他任意の種類のストールによって遅れる場合、レジスタから自由に読み出せる期間は命令ストリームの下流に延びる。

例:

         MOV EAX, EBX
         SUB ECX, EAX
         INC EBX
         MOV EDX, [EAX]
         ADD ESI, EBX
         ADD EDI, ECX

これら六つの命令は各々一つのμ-OPSを生成する。最初の三つのμ-OPSが一緒にRATに行ったとしよう。これらの三つのμ-OPSはレジスタEBX, EBX, EAXを読み出す。しかしEAXは読み出す前に書き込もうとするため、読み出しは自由でストールはない。次の三つのμ-OPSはEAX, ESI, EBX, EDI, ECXから読み出す。EAX, EBXとECXは先の三つ組によって変更されており、まだ書き戻してないため、これらは自由に読み出せる。それでESIとEDIだけが読み出しの対象となり、次の三つ組でもストールを受けない。最初の三つ組のSUB ECX,EAXをCMP ECX, EAXと置き換えると、ECXは書かれないので次の三つ組ESI, EDI, ECXの読み出し時にストールを受ける。同様に、INC EBXをNOPか何かで置き換えると、次の三つ組でESI, EBX, EDXの読み出し時にストールを受ける。

どのμ-OPSも、三つ以上のレジスタから読み出すことはできない。それゆえ、三つ以上のレジスタを読み出す全ての命令は二つ以上のμ-OPSに分解される。

レジスタの読み出し数を数えるには、命令によって読み出されるレジスタの数を含めなければならない。これは整数レジスタ、フラグレジスタ、スタックポインタ、浮動小数点レジスタ、そしてMMXレジスタである。XMMレジスタは二つのレジスタと数える。但し、一部分だけを使う場合を除く。例えば、ADDSSとMOVHLPS命令である。セグメントレジスタと命令ポインタは数えない。例えば、SETZ ALにおいて、フラグレジスタは数えるが、ALは数えない。ADD EBX, ECXでは、EBXとECXの両方を数える。しかしフラグレジスタは数えない。なぜならフラグレジスタは書き込まれるだけだからである。PUSH EAXはEAXとスタックポインタを読み込み、スタックポインタに書き込む。

FXCH命令は特別な場合である。これはリネーミングによって動くが、どんな値も読み出さない。それでレジスタ・リード・ストールの規則に数えない。FXCH命令はレジスタの読み書きを一切しないので、レジスタ・リード・ストールの規則に関しては一つのμ-OPSのように振る舞う。

μ-OPS三つ組と、デコードグループを混同しないでいただきたい。デコードグループは一つから六つまでのμ-OPSを生成する。デコードグループが三つの命令を持ち、三つのμ-OPSを生成する時でさえ、三つのμ-OPSが一緒にRATに行くという保証はない。

デコーダとRAT間のキューは大変短い(十個のμ-OPS)ので、レジスタ・リード・ストールがデコーダをストールさせたり、デコーダのスループットの変動がRATをストールさせたりしないとは見なせない。

キューが空でない限り、どのμ-OPSが一緒にRATに行くかを予測することは大変難しい。最適化されたコードでは分岐予測ミスが起きた直後でだけキューが空になっているべきである。同じ命令によって生成されるいくつかのμ-OPSは必ずしも一緒にRATに行くわけではない。というのは、μ-OPSは単にキューから順番に、一度に三つずつ取り上げられるからである。この連続は予測された分岐によって壊されることはない。ジャンプの前後のμ-OPSも一緒にRATに行くことができる。予測ミスした分岐でだけは、キューの内容が捨られて最初からやり直しになるので、次の三つのμ-OPSは確実に一緒にRATに行く。

三つの連続するμ-OPSが三つ以上の異なるレジスタから読み出す時、当然それらは一緒にRATを通らないほうがよいと思うだろう。一緒に通る確率は1/3である。三つ組のμ-OPS中で、書き戻されたレジスタから三つまたは四つ読み出すことによるペナルティは、1クロックサイクルである。1クロックの遅れは、RATを通してあと三つのμ-OPSを読み出すのと等価と思ってよい。三つのμ-OPSが一緒にRATに行く確率が1/3であるため、ペナルティの平均は3/3=1μ-OPSと等価になるはずである。コード片がRATを通るのにかかる平均時間を計算するためには、レジスタ・リード・ストールを起こす可能性のある数をμ-OPSに加算し、3で割ればよい。どのμ-OPSが一緒にRATに行くかが確実にわかっているか、または余計な一つの命令によって二つ以上のレジスタ・リード・ストールの可能性を防ぐのでなければ、余計な命令を一つ挿入することによってストールを取り除くことは引き合わないと分かるであろう。

1クロック当たり三つのμ-OPSのスループットを狙う状況では、1クロックサイクル当たり二つしか常置レジスタから読み出せない制限は、扱うのに問題となるボトルネックであろう。レジスタ・リード・ストールを取り除く可能性のある方法は、

二つ以上のμ-OPSを生成する命令において、レジスタ・リード・ストールの可能性の正確な分析をするため、命令から生成されるμ-OPSの順序が知りたいかもしれない。それで、最も普通の例を下に並べてみた。

メモリへの書き込み
メモリへの書き込みは二つのμ-OPSを生成する。一つ目(ポート4へ)はストア動作で、ストアするレジスタを読み出す。二つ目(ポート3)はポインタレジスタを読み出してメモリの番地を計算する。例:

    MOV [EDI], EAX
最初のμ-OPSはEAXを読み出し、二番目のμ-OPSはEDIを読み出す。
    FSTP QWORD PTR [EBX+8*ECX]
最初のμ-OPSはST(0)を読み出し、二番目のμ-OPSはEBXとECXを読み出す。

読み出しと変更
メモリ・オペランドを読み出して、何らかの演算または論理操作によりレジスタを変更する命令は、二つのμ-OPSを生成する。最初のμ-OPS(ポート2)はロード命令で、ポインタレジスタを読み出す。二番目のμ-OPSは演算命令(ポート0または1)で、被演算レジスタの読み出しと書き込みを行い、フラグへの書き込みの可能性もある。例:

    ADD EAX, [ESI+20]
最初のμ-OPSはESIを読み出し、二番目のμ-OPSはEAXを読み出して、EAXとフラグに書き込む。

読み出し/変更/書き込み
読み出し/変更/書き込み命令は四つのμ-OPSを生成する。最初のμ-OPS(ポート2)は何らかのポインタレジスタを読み出し、二番目のμ-OPS(ポート0または1)はソースレジスタから読み出しと書き込み(訳注: 一時結果の書き込みか)を行い、フラグへの書き込みの可能性がある。三番目のμ-OPS(ポート4)は一時的な結果だけ(ここでは数に入れない)を読み出し、四番目のμ-OPS(ポート3)は再度何らかのポインタレジスタを読み出す。最初のμ-OPSと四番目のμ-OPSは一緒にRATに入れないため、同一のポインタレジスタを用いることによる有利性がない。例:

    OR [ESI+EDI], EAX
最初のμ-OPSはESIとEDIを読み出す。二番目のμ-OPSはEAXを読み出しEAXとフラグに書き込む。三番目のμ-OPSは一時的な結果だけを読み出す。四番目のμ-OPSはESIとEDIを再度読み出す。これらのμ-OPSがたとえどのようにRATに入ろうと、EAXを読み出すμ-OPSは、ESIとEDIを読み出すμ-OPSのいずれか一方と一緒に行くと確信してよい。それゆえレジスタ・リード・ストールはこの命令においては、これらのレジスタのどちらかが最近変更されていなければ避けることができない。

レジスタのプッシュ
レジスタのプッシュ命令は三つのμ-OPSを生成する。最初のμ-OPS(ポート4)は、レジスタを読み出すストア命令である。二番目のμ-OPS(ポート3)はスタックポインタを読み出し、番地を生成する。三番目のμ-OPS(ポート0または1)はスタックポインタを読み出して変更し、ワードの大きさをスタックポインタから引く。

レジスタのポップ
レジスタのポップ命令は二つのμ-OPSを生成する。最初のμ-OPS(ポート2)は値のロードで、スタックポインタを読み出してレジスタに書き込む。二番目のμ-OPS(ポート0または1)はスタックポインタを読み出して変更し、スタックポインタを調整する。

呼び出し(コール)
nearコールは四つのμ-OPSを生成する(ポート1, 4, 3, 01)。最初の二つのμ-OPSはIPレジスタから読むだけ(リネームできないので数に入らない)である。三番目のμ-OPSはスタックポインタを読み出す。最後のμ-OPSはスタックポインタを読み出しで変更する。

復帰(リターン)
nearリターンは四つのμ-OPSを生成する(ポート2, 01, 01, 1)。最初のμ-OPSはスタックポインタを読み出す。三番目のμ-OPSはスタックポインタを読み出しで変更する。

レジスタ・リード・ストールを避ける方法は例2.6に書いてある。


17. アウト・オブ・オーダー実行 (PPro, PII and PIII)

リオーダ・バッファ(ROB)は40個のμ-OPSを保持できる。各々のμ-OPSはすべてのオペランドの準備ができ、実行ユニットに空きができるまでROBで待機する。これはアウト・オブ・オーダー実行を可能にする。キャッシュ・ミスによる遅延がコード部分に生じた時も、それより後のコード部分が遅らされた操作と独立であれば、それらが遅延を受けることはない。

メモリへの書き込みは、他の書き込みと比べてアウト・オブ・オーダー実行することはできない。四つの書き込みバッファがあるので、多くのキャッシュ・ミスやキャッシュされていないメモリに書き込んでいることが予期されるならば、四つの書き込みを同時に行い、次の四つの書き込みの前にプロセッサに何か他の仕事を確実にさせるようにスケジュールすることを勧める。メモリの読み出しと他の命令は、IN命令、OUT命令、その他のシリアル化命令を除いてアウト・オブ・オーダー実行できる。

コードがメモリへの書き込みをし、そのすぐ後に同じ番地から読み出しをする場合、読み出しは誤って書き込みよりも先に実行されることがある。というのは、ROBは並べ替えをしている時はメモリの番地がわからないからである。このエラーは書き込み番地が計算される時に検出され、読み出し動作(これは投機実行であった)は再実行されなければならない。このペナルティはおおよそ3クロックである。このペナルティを避ける唯一の方法は、同じ番地のメモリへの書き込みと引き続く読み出しの間に実行ユニットに他の仕事を確実にさせることである。

五つのポートの周りにはいくつかの実行ユニットが配置されている。ポート0と1は演算操作などを行う。単純移動、演算と論理操作はポート0と1の両方ででき、どちらか空いた方で先に実行される。ポート0は乗算、除算、整数シフトと回転、そして浮動小数点操作も扱える。ポート1はジャンプといくつかのMMX, XMM操作を行うことができる。ポート2はメモリから読み出しのすべてと少しのストリング命令、XMM命令を扱うことができる。ポート3はメモリ書き込みの番地の演算を行う。ポート4はすべてのメモリ書き込みの操作を行う。29章に命令によって生成されるμ-OPSと、それらが行くであろうポートの完全な表がある。すべてのメモリ書き込み命令は二つのμ-OPS、一つはポート3、もう一つはポート4、を必要とすることに注意して欲しい。それに比べメモリ読み出し命令は一つのμ-OPSだけ(ポート2)を使用する。

ほとんどの場合、各々のポートは1クロックサイクル当たり一つの新しいμ-OPSを受け取ることができる。これは、五つのμ-OPSが別々のポートに行けば、同じクロックサイクル内に五つのμ-OPSまで実行できることを意味している。しかしパイプラインの最初のほうで1クロック当たり最大三つまでのμ-OPSまでという制限があるため、平均して1クロック当たり三つのμ-OPSを超えて実行することはできない。

1クロック当たり三つのμ-OPSのスループットを維持したいならば、どの実行ポートもμ-OPSの1/3を超えて受け取ることはないということを確かめる必要がある。29章のμ-OPSの表を用い、各々のポートに行くμ-OPSの数を数えよ。ポート0と1が満たされており、ポート2が空いていれば、ポート0と1からのロードのどちらかをポート2からのロードにするため、MOV レジスタ, レジスタまたはMOV レジスタ, 即値の命令のどちらかをMOV レジスタ, メモリ命令に置き換えることにより、コードを改善することができる。

大部分のμ-OPSは実行に1クロックサイクルしかかからないが、乗算、除算、そして多くの浮動小数点命令はもっとかかる。

浮動小数点の加算と減算には3クロックサイクルかかるが、先の命令が終了する前に新しいFADDやFSUB命令を受け取るように実行ユニットは完全にパイプライン化されている(もちろん、それらが独立である時だが)。

整数の乗算には4クロックかかり、浮動小数点の乗算は5クロック、MMX操作は3クロックかかる。整数とMMXの操作は、クロックサイクル毎に新しい命令を受け取ることができるようにパイプライン化されている。浮動小数点の乗算は部分的にパイプライン化されている。実行ユニットは新しいFMUL命令を先の命令の2クロック後に受け取ることができる。それで最大のスループットは2クロックサイクル当たり一つのFMULである。FMUL同士の間を整数の乗算で埋めることはできない。というのは、それらは同じ回路を使うからである。XMMの加算と乗算はそれぞれ3クロックと4クロックで、完全にパイプライン化されている。しかし各々の論理XMMレジスタは二つの64ビット物理レジスタとして実装されているため、パック化されたXMM操作に二つのμ-OPSを必要とし、スループットは2クロックサイクル当たり一つのXMM演算命令である。XMMの加算と乗算命令は並列に実行できる。というのは、それらは同じ実行ポートを使わないからである。

整数と浮動小数点の除算は最大39クロックかかり、パイプライン化されていない。これは先の除算が終わらないと実行ユニットは新しい除算を始めることができないということを意味している。同じ事が平方根と超越数の関数に適用される。

ジャンプ命令、呼び出し(コール)命令、復帰(リターン)命令も同様に完全にパイプライン化されていない。先のジャンプ命令の1クロックサイクル後に新しいジャンプを始めることはできない。それでジャンプ命令、呼び出し命令、復帰命令の最大スループットは2クロックサイクル当たり1命令となる。

もちろん、多くのμ-OPSを生成する命令は避けなければならない。LOOP XX命令は、例えば、DEC ECX / JNZ XXで置き換えるべきである。

連続したPOP命令は、μ-OPSの数を減らすために分解してもよい。例えば

    POP ECX / POP EBX / POP EAX     ; これは次のように替えられる
    MOV ECX,[ESP] / MOV EBX,[ESP+4] / MOV EAX,[ESP] / ADD ESP,12 ; 訳注: MOV EAX,[ESP+8]の間違いであると思われる
先のコードは六つのμ-OPSを生成するが、後の命令はわずか四つのμ-OPSを生成し、デコードが速い。PUSH命令を同じように分解することは有利でない。というのは分解されたコードは、それらの間に別の命令を挿入するか、レジスタが最近リネームされていないと、レジスタ・リード・ストールを起こしがちだからである。呼び出し(コール)命令と復帰(リターン)命令を同様に分解すると、リターン・スタック・バッファの予測の妨げになる。ADD ESP命令は初期のプロセッサではAGIの原因となることにも注意せよ。


18. リタイアメント (PPro, PII and PIII)

リタイアメントは、μ-OPSによって使われたテンポラリレジスタを常置レジスタ(EAX, EBXなど)に移す処理である。μ-OPSが実行されてしまうと、それはROB内でリタイアの準備ができた印が付けられる。

リタイアメント・ステーションは1クロックサイクル当たり三つのμ-OPSを扱うことができる。RAT内でスループットは既に1クロック当たり三つのμ-OPSに制限されているので、これは問題に見えないかもしれない。しかし、リタイアメントは二つの理由のためにまだなおボトルネックになりうる。まず、命令は順序正しくリタイアしなければならない。μ-OPSをアウト・オブ・オーダー実行すると、順序に従ったすべての先行するμ-OPSがリタイアしないと、リタイアできない。二番目の制限は、ジャンプ命令はリタイアメント・ステーションの三つのスロットの内最も最初にリタイアしなければならないことである。丁度次の命令がD0だけに収まる場合D1とD2デコーダが空になるように、次のリタイアするμ-OPSがジャンプ命令で取られるとリタイアメント・ステーション内の後の二つのスロットが空になる。これは、μ-OPSの数が3で割り切れない命令を持つ小さなループの場合重大である。

リオーダ・バッファ(ROB)内の全てのμ-OPSはリタイアするまでとどまる。ROBは40個のμ-OPSを保持できる。これは長い遅延を伴う命令または遅い命令の間に実行できる命令の数を制限する。除算が終了する前にROBはリタイアを待つ実行されたμ-OPSで満たされる。除算が終了、リタイアした時にのみ、連続するμ-OPSはリタイアを開始できる。というのはリタイアは順序に従って実行されるからである。

予測分岐の投機実行の場合(22章を見よ)、投機実行されたμ-OPSは予測が正しいことが確実になるまでリタイアすることができない。予測が外れた時は投機実行されたμ-OPSはリタイアすることなく捨てられる。

次の命令は投機実行できない。メモリへの書き込み、IN, OUT命令、シリアル化命令がそうである。


19. パーシャル・ストール (PPro, PII and PIII)

19.1 パーシャル・レジスタ・ストール

パーシャル・レジスタ・ストールは、32ビットレジスタの一部分に書いた後、レジスタ全体または書いたサイズより大きいサイズで読み出そうとした時に起きる問題である。例えば
        MOV AL, BYTE PTR [M8]
        MOV EBX, EAX            ; パーシャル・レジスタ・ストール

これは5〜6クロックの遅延を伴う。この理由は、ALにテンポラリレジスタが割り当てられるからである(AHとは独立するように)。実行ユニットはALとEAXの残りの値とを結合できるようになる前に、ALへの書き込みがリタイアするまで待たなければならない。ストールはコードを次のように替えることによって避けられる。

        MOVZX EBX, BYTE PTR [MEM8]
        AND EAX, 0FFFFFF00h
        OR EBX, EAX

もちろんパーシャル・ストールは、パーシャル・レジスタに書いた後に別の命令を挿入することによって避けることもできる。これはフルサイズのレジスタを読む前に、リタイアまでの時間を稼ぐためである。

違うサイズのデータ(8, 16, そして32ビット)を混ぜる時はいつも、パーシャル・ストールが起きることに気を付けておくべきである。

        MOV BH, 0
        ADD BX, AX              ; ストールあり
        INC EBX                 ; ストールあり

フルサイズのレジスタ、またはより大きなサイズでレジスタに書き込んだ後でパーシャル・レジスタを読み出しても、ストールは生じない。

        MOV EAX, [MEM32]
        ADD BL, AL              ; ストールなし
        ADD BH, AH              ; ストールなし
        MOV CX, AX              ; ストールなし
        MOV DX, BX              ; ストールあり

パーシャル・レジスタ・ストールを避ける最も簡単な方法は、いつもフルサイズのレジスタを用い、より小さなメモリ・オペランドより読み出す時にMOVZXまたはMOVSXを用いることである。これらの命令はPPro, PIIとPIIIでは速いが、初期のプロセッサでは遅い。それゆえ、すべてのプロセッサで適度にコードが働くようにしたい場合の妥協案を示す。MOVZX EAX,BYTE PTR [M8]の代わりに以下のようにすればよい。

        XOR EAX, EAX
        MOV AL, BYTE PTR [M8]

この組み合わせはPPro, PIIとPIIIで、後でEAXから読み出す場合にパーシャル・レジスタ・ストールを避ける特別な場合である。秘訣は、レジスタ自身でXORを取れば、レジスタには空というタグが付けられる所にある。プロセッサはEAXの上位24ビットがゼロであることを覚えており、パーシャル・ストールは避けることができる。この機構は一定の組み合わせの時のみ働く。

        XOR EAX, EAX
        MOV AL, 3
        MOV EBX, EAX            ; ストールなし

        XOR AH, AH
        MOV AL, 3
        MOV BX, AX              ; ストールなし

        XOR EAX, EAX
        MOV AH, 3
        MOV EBX, EAX            ; ストールあり

        SUB EBX, EBX
        MOV BL, DL
        MOV ECX, EBX            ; ストールなし

        MOV EBX, 0
        MOV BL, DL
        MOV ECX, EBX            ; ストールあり

        MOV BL, DL
        XOR EBX, EBX            ; ストールなし

レジスタをゼロにするのに、レジスタそれ自身から減算することはXORを実行するのに等しいが、MOV命令を用いてゼロにした場合はストールを妨げることはできない。

XORをループの外に置くことができる。

        XOR EAX, EAX
        MOV ECX, 100
LL:     MOV AL, [ESI]
        MOV [EDI], EAX          ; ストールなし
        INC ESI
        ADD EDI, 4
        DEC ECX
        JNZ LL

プロセッサは割り込み、分岐予測ミス、シリアル化イベントが起きない限り、EAXの上位24ビットがゼロであることを覚えている。

レジスタ全体をプッシュするかもしれないサブルーチンを呼び出す前に、パーシャル・レジスタをすべて中和することを覚えておくべきである。

        ADD BL, AL
        MOV [MEM8], BL
        XOR EBX, EBX            ; BLを中和する
        CALL _HighLevelFunction

大部分の高級言語の手続きは、上で述べたようにBLレジスタを中和しておかないと、パーシャル・レジスタ・ストールを生じるようなEBXレジスタのプッシュを、手続きの最初で行う。

レジスタをXORでゼロにする手法は、それより前の命令との依存性を断ち切ることはできない。

        DIV EBX
        MOV [MEM], EAX
        MOV EAX, 0              ; 依存性を断ち切る
        XOR EAX, EAX            ; パーシャル・レジスタ・ストールを妨げる
        MOV AL, CL
        ADD EBX, EAX

EAXを二回ゼロにすることは無駄に見えるかもしれないが、MOV EAX,0なしでは、最後の命令は遅いDIV命令が終わるまで待たなければならず、XOR EAX,EAXなしではパーシャル・レジスタ・ストールが生じる。

FNSTSW AX命令は特別である。32ビットモードではEAX全体に書き込むように振る舞う。実際は、このようなことを32ビットモードで行っている:

    AND EAX,0FFFF0000h / FNSTSW TEMP / OR EAX,TEMP

これゆえ、32ビットモードではこの命令の後でEAXから読み出してもパーシャル・レジスタ・ストールは生じない。

    FNSTSW AX / MOV EBX,EAX         ; 16ビットモード時のみストールあり
    MOV AX,0  / FNSTSW AX           ; 32ビットモード時のみストールあり

19.2 パーシャル・フラグ・ストール

フラグも同様パーシャル・レジスタ・ストールの原因となる。
        CMP EAX, EBX
        INC ECX
        JBE XX          ; パーシャル・フラグ・ストール

JBE命令はキャリーフラグとゼロフラグの両方を読む。INC命令はゼロフラグを変化させるが、キャリーフラグを変化させない。JBE命令は、CMP命令からのキャリーフラグとINC命令からのゼロフラグを結合できるまで、前の二つの命令がリタイアするのを待たなければならない。この状態は意図的なフラグの組み合わせというよりはむしろバグである可能性が高い。これを修正するには、INC ECXをADD ECX,1に替えればよい。似たようなパーシャル・フラグ・ストールの原因となるバグは、SAHF / JL XXである。JL命令はサインフラグとオーバーフローフラグを調べるが、SAHF命令はオーバーフローフラグを変化させない。これを修正するには、JL XXをJS XXに替えればよい。

思いがけいことに(インテルの説明書で述べられていることに反して)、フラグビットのいくつかを変更する命令の後で、変更しなかったフラグビットだけを読んだ時に、パーシャル・フラグ・ストールを受けることがある。

        CMP EAX, EBX
        INC ECX
        JC  XX          ; パーシャル・フラグ・ストール

しかし、変更したビットのみを読めばこれは起きない。

        CMP EAX, EBX
        INC ECX
        JE  XX          ; ストールなし

パーシャル・フラグ・ストールは多くのまたはすべてのフラグビットを読んだ時に起きやすい。すなわち、LAHF, PUSHF, PUSHFDである。次の命令群は、LAHF, PUSHF(D)命令が後に置かれた時にパーシャル・フラグ・ストールを起こしやすい。INC, DEC, TEST, ビットテスト、ビットスキャン、CLC, STC, CMC, CLD, STD, CLI, STI, MUL, IMUL、そしてすべてのシフトと回転命令である。次の命令群はパーシャル・フラグ・ストールの原因とはならない。AND, OR, XOR, ADD, ADC, SUB, SBB, CMP, NEG命令。TEST命令とAND命令は、定義によればフラグに全く同じ動作をするにも関わらず、振る舞いが異なるのは奇妙である。フラグの値をストアする際に、ストールを避けるため、LAHF命令やPUSHF(D)命令の替わりにSETcc命令を用いてもよい。

例:

    INC EAX   / PUSHFD      ; ストールあり
    ADD EAX,1 / PUSHFD      ; ストールなし

    SHR EAX,1 / PUSHFD      ; ストールあり
    SHR EAX,1 / OR EAX,EAX / PUSHFD   ; ストールなし

    TEST EBX,EBX / LAHF     ; ストールあり
    AND  EBX,EBX / LAHF     ; ストールなし
    TEST EBX,EBX / SETZ AL  ; ストールなし

    CLC / SETZ AL           ; ストールあり
    CLD / SETZ AL           ; ストールなし

パーシャル・フラグ・ストールのペナルティはおおよそ4クロックである。

19.3 シフト命令・回転命令の後のフラグ・ストール

シフト命令または回転命令の後で、何らかのフラグが読まれると、パーシャル・フラグ・ストールに似たストールが起きることがある。但し、1ビットのみのシフトまたは回転(短縮形)を除く。
    SHR EAX,1 / JZ XX                ; ストールなし
    SHR EAX,2 / JZ XX                ; ストールあり
    SHR EAX,2 / OR EAX,EAX / JZ XX   ; ストールなし

    SHR EAX,5 / JC XX                ; ストールあり
    SHR EAX,4 / SHR EAX,1 / JC XX    ; ストールなし

    SHR EAX,CL / JZ XX               ; CL = 1でもストールあり
    SHRD EAX,EBX,1 / JZ XX           ; ストールあり
    ROL EBX,8 / JC XX                ; ストールあり

これらのストールのペナルティはおおよそ4クロックである。

19.4 パーシャル・メモリ・ストール

パーシャル・メモリ・ストールはいくらかパーシャル・レジスタ・ストールに似ている。それは、同じメモリ番地でサイズの違うデータを扱った時に起きる。
        MOV BYTE PTR [ESI], AL
        MOV EBX, DWORD PTR [ESI]        ; パーシャル・メモリ・ストール

ここでプロセッサはALから書かれたバイトと、次の3バイトを結合するためにストールが起きる。3バイトは前からメモリにあった値で、EBXに読み込むために4バイト得るために必要である。このペナルティはおおよそ7〜8クロックである。

パーシャル・レジスタ・ストールに似ていないが、より大きなサイズのオペランドを書いたあと、その一部分を違う番地で始まる所から読めば、パーシャル・メモリ・ストールが起きる。

        MOV DWORD PTR [ESI], EAX
        MOV BL, BYTE PTR [ESI]          ; ストールなし
        MOV BH, BYTE PTR [ESI+1]        ; ストールあり

このストールは、最後の行をMOV BH,AHで置き換えれば避けることができる。しかし、次のような状況は解決できない。

        FISTP QWORD PTR [EDI]
        MOV EAX, DWORD PTR [EDI]
        MOV EDX, DWORD PTR [EDI+4]      ; ストールあり

面白いことに、書き込みと読み出しが全く異なる番地の場合もパーシャル・メモリ・ストールが起きることがある。これは、同じセット値を持つ異なるキャッシュ・バンクに対して起こる。

        MOV BYTE PTR [ESI], AL
        MOV EBX, DWORD PTR [ESI+4092]   ; ストールなし
        MOV ECX, DWORD PTR [ESI+4096]   ; ストールあり


20. 依存の連鎖 (PPro, PII and PIII)

前の各々の命令の結果に依存した命令の連続を、依存の連鎖と呼ぶ。長い依存の連鎖はできれば避けるべきである。というのは、アウト・オブ・オーダー実行と並列実行の妨げになるからである。

例:

   MOV EAX, [MEM1]
   ADD EAX, [MEM2]
   ADD EAX, [MEM3]
   ADD EAX, [MEM4]
   MOV [MEM5], EAX

この例では、ADD命令は各々二つのμ-OPSを生成する。一つはメモリからの読み出し(ポート2)、もう一つは加算(ポート0または1)である。読み出しμ-OPSはアウト・オブ・オーダー実行できるが、加算のμ-OPSは前のμ-OPSが終了するまで待たなければならない。この依存関係の連続はそんなに長い実行時間を必要としない。というのは各々の加算は1クロックしかかからないからである。しかしこれが乗算のような遅い命令、またはもっと悪い場合、例えば除算などなら、依存の連鎖を壊すためにぜひとも何らかの手を打たなければならない。これには、多数のアキュムレータを使う方法がある。

   MOV EAX, [MEM1]         ; 最初の連鎖のスタート
   MOV EBX, [MEM2]         ; 他のアキュムレータによる連鎖のスタート
   IMUL EAX, [MEM3]
   IMUL EBX, [MEM4]
   IMUL EAX, EBX           ; 最後に連鎖を結合する
   MOV [MEM5], EAX

ここで、二番目のIMUL命令は最初の命令が終了する前に始めることができる。IMUL命令が4クロックかかり、完全にパイプライン化されているので、4つまでのアキュムレータを使ってよい。

除算はパイプライン化されていないので、除算の連鎖に同じ手を使うことはできない。しかしもちろんすべての除数を掛け合わせて最後に一度だけ割ってよい。

浮動小数点命令は整数命令より長い遅延があるため、ぜひとも長い浮動小数点の依存の連鎖を壊すべきである。

   FLD [MEM1]         ; 最初の連鎖のスタート
   FLD [MEM2]         ; 異なるアキュムレータによる二番目の連鎖のスタート
   FADD [MEM3]
   FXCH
   FADD [MEM4]
   FXCH
   FADD [MEM5]
   FADD               ; 最後に連鎖を結合する
   FSTP [MEM6]

このために多くのFXCH命令が必要であるが、心配はない。これは安価である。FXCH命令はRATの中でレジスタ・リネーミングによって解決される。それで実行ユニットに何らのロードもない。もっとも、FXCHはRAT, ROB, リタイアメント・ステーションで1μ-OPSとして数えられる。

依存の連鎖が長い時は三つのアキュムレータが必要になる。

        FLD [MEM1]              ; 最初の連鎖のスタート
        FLD [MEM2]              ; 二番目の連鎖のスタート
        FLD [MEM3]              ; 三番目の連鎖のスタート
        FADD [MEM4]             ; 三番目の連鎖
        FXCH ST(1)
        FADD [MEM5]             ; 二番目の連鎖
        FXCH ST(2)
        FADD [MEM6]             ; 最初の連鎖
        FXCH ST(1)
        FADD [MEM7]             ; 三番目の連鎖
        FXCH ST(2)
        FADD [MEM8]             ; 二番目の連鎖
        FXCH ST(1)
        FADD                    ; 最初と三番目の連鎖の結合
        FADD                    ; 二番目の連鎖の結合
        FSTP [MEM9]

即値をメモリに書き込んだ直後に読み出すのは避けよ。

        MOV [TEMP], EAX
        MOV EBX, [TEMP]

直前のメモリの書き込みが終了する前に同じメモリ番地から読もうとする試みにはペナルティがある。上の例で、最後の命令をMOV EBX,EAXに変えるか、または間に何か別の命令を挟むとよい。

整数レジスタから浮動小数点レジスタへと変換する、またはその逆の、メモリに中間データを書き込むことが避けられない一つの状況がある。例えば

        MOV EAX, [MEM1]
        ADD EAX, [MEM2]
        MOV [TEMP], EAX
        FILD [TEMP]

TEMPへの書き込みとTEMPからの読み出しの間に何も入れないならば、EAXの替わりに浮動小数点レジスタを使うことを考えてもよい。

        FILD [MEM1]
        FIADD [MEM2]

連続したジャンプ、呼び出し、復帰もまた依存の連鎖と考えられる。これらの命令のスループットは2クロック当たり1ジャンプである。それゆえ、ジャンプ命令の間にプロセッサに何か他のことをさせるのが望ましい。


21. ボトルネックを探す (PPro, PII and PIII)

これらのプロセッサのコードを最適化する際に、ボトルネックがある場所がどこかを解析するのは重要である。もっと狭いボトルネックが他にあるのに、一つのボトルネックを取り去るために時間を費やすのは意味がない。

コードのキャッシュ・ミスが予測されるなら、コードのよく使われる部分が一緒になるように再構築するとよい。

多くのデータ・キャッシュ・ミスが予測されるなら、他のことは忘れてキャッシュ・ミスの数を減らすようデータを再構築すること(7章)と、データのリード・キャッシュ・ミスの後の長い依存の連鎖を避けること(20章)に集中せよ。

多くの除算があるなら、それらを減らすよう努力し(27章2)、除算の最中にプロセッサに何か他の仕事を確実にするようにせよ。

依存の連鎖はアウト・オブ・オーダー実行の邪魔になりがちである(20章)。特にそれが乗算、除算、浮動小数点命令のような遅い命令を含んでいるならば、長い依存の連鎖を壊すように努めよ。

多くのジャンプ、呼び出し、復帰があるなら、そして特にジャンプが予測しにくいならば、それらを避けるように試みよ。可能なら条件分岐を条件移動に、小さな手続きはマクロに(23章2)置き換えよ。

異なるサイズのデータ(8, 16 ,32ビットの整数)が混ざっているなら、パーシャル・ストールに気を付けよ。PUSHFまたはLAHF命令を使っているなら、パーシャル・フラグ・ストールに気を付けよ。2ビット以上のシフトまたは回転命令の後でフラグを調べるのは避けよ(19章)。

1クロックサイクル当たり三つのμ-OPSのスループットを狙うならば、命令の取り込みとデコードの遅延を知るようにせよ(14章15章)。特に小さなループには気を付けよ。

常置レジスタからの1クロックサイクル当たりの読み出しが二つまでという制限は、1クロックサイクル当たり三つのμ-OPSのスループットを減らすことがある(16章2)。これはレジスタが更新されてから4クロックサイクルより後に度々読み出すと起きやすい。これは、例えばデータの番地のためのポインタを度々使うが滅多にポインタを更新しない時に起きる。

1クロック当たり三つのμ-OPSのスループットを得るには、実行ユニットがμ-OPSの1/3を超えて受け取らないことが必要である(17章)。

リタイアメント・ステーションは1クロック当たり三つのμ-OPSを扱うことができるが、分岐するジャンプ命令にはあまり有効ではない(18章)。


22. ジャンプと分岐 (すべてのプロセッサ)

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

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

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

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

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

22.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パターンからはずれるまで続く。これがこの分岐予測機構で最も悪い場合である。

22.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の情報だけに頼っている。

命令は、初回に実行されるときにはめったにペアにならないことを、ここで思い出すかもしれない(8章参照)。上の命令がペアにならなければ、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を比べなければならない。これはたいへんあきあきするし、誰かがやっていると聞いたこともない。この仕事をあなたの代わりにやってくれるような、利用可能なツールはない。

22.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では従って、ジャンプする番地のリストがよい解かもしれない。ジャンプする番地のリストはデータセグメントに置くべきである。決してデータをコードセグメントに置くな!

22.1.4 きついループ(PPlain)

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

22.2 PMMX、PPro、PII、PIIIの分岐予測

22.2.1 BTBの構成(PMMX, PPro PII and PIII)

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

PPro、PIIとPIIIの branch target buffer は512個のエントリを持ち、それは16ウェイ×32セットの構成になっている。各エントリは、それの属する制御移行命令の最後のバイトのビット4〜31で特定される。ビット4〜8がセットを定義し、BTBに入れられたすべてのビットがタグとなる。512バイト離れた制御移行命令同士は同じセット値を持つので、時には互いに相手をBTBから押し出したりする。セット毎に16のウェイがあるので、これはそんなには起きないだろう。

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

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

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

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

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

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

これらのプロセッサは、例えば、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個のカウンタは使わない。

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

以下は完全に予測できる繰り返しの分岐パターンのリストである。
周期       パターン
-----------------------------------------------------------------------------
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になる。これはもちろん新しいパターンではなく、同じパターンの相がずれたものである。表の中のあるパターンから反転したり、ビット反転したり、回転したりして導出できるパターンもすべて認識できる。簡潔にするために、これらはリストされていない。

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

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

分岐予測機構は「ほとんど規則的な」パターンや、規則的なパターンからのはずれを扱うのも得意である。分岐予測機構は、規則的なパターンがどのように見えるか学習するだけではない。規則的なパターンからのはずれがどのように見えるかも学習する。もしはずれ方がいつも同じなら、不規則なできごとの後で何が来るかを覚え、はずれのコストは予測ミス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回だけの予測ミスで、パターンの変化も扱えるように学習してしまう。

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

完全には予測できない最も単純な分岐パターンは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の両方で予測ミスが起きるという不運な成り行きとなる。(これは
22章1.1の終わりで説明したPPlainの最悪の場合と同類である)。この四つの状況のどれになるかは、この分岐へ割り当てる前の、BTBエントリの履歴による。ランダム割り当て法のため、これは制御できない。

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

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

パターン認識の強力な能力には、規則性の全然ない完全にランダムな列の場合には小さな欠点がある。 次の表は、ジャンプする/しないの完全にランダムな列についての予測ミスの、実験的に求めた割合を表している。
ジャンプする/しない 予測ミスの割合
---------------------------------------
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
---------------------------------------

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

22.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とPIIIのきついループについては通常通り予測され、繰り返し毎に少なくとも2クロックサイクルかかる。

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

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

22.2.10 JECXZとLOOP(PMMX)

PMMXには、これら二つの命令のためのパターン認識機構はない。単純に、前回の実行と同じようになると予測するだけである。これら二つの命令は、PMMXの、時間的に決定的なコードでは、避けるべきである(PPro, PIIとPIIIではパターン認識による予測が行われるが、それでもループ命令はDEC ECX / JNZよりは劣る。)。

22.2.11 リターン(PMMX, PPro, PII and PIII)

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

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

PMMXでは、RSBは四つのエントリ、PPro、PIIとPIIIでは16エントリしか保持できない。RSBが空のときには、リターン命令は間接ジャンプと同じように、つまり、前回と同じジャンプ先に行くと予測される。

サブルーチンが4段より深くネストするときは、最も内側の4段がRSBを使い、それより外側からのリターンでは、新しいコールがない限り、単純な予測機構を使う。RSBを使っているリターン命令もBTBエントリを専有する。RSBの四つのエントリは多くないと思うかもしれないが、おそらく十分である。4段より深いサブルーチンのネスティングはたしかに珍しくはないが、速度については、最も深い部分だけが問題になる。

PPro、PIIとPIIIでは、サブルーチンが16段より深くネストする時は、最も内側の16段がRSBを用いるが、それより外側に続くリターンでは、予測ミスする。それゆえ再帰的なサブルーチンは16段より深くするべきではない。

22.2.12 静的予測(PMMX)

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

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

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

例:
        JMP SHORT LL
LL:

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

22.2.13 静的予測(PPro, PII and PIII)

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

コードがキャッシュされそうにない時は、最もしばしば分岐する命令は後ろの方へ置くのが望ましい。これは命令取り込みを改善するためである。

22.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を二つ入れてペアリングを防ぐようにせよ(22章1.3参照)。

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

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

22.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の更新が必要なため)。リターンの連鎖はいつでもペナルティがある。コールの後のジャンプには時々小さなストールがあるが、コールの後のリターン、リターンの後のコール、ジャンプの後のジャンプとコールとリターン、リターンの後のジャンプには、ペナルティはない。

22.2.16 連続したジャンプ (PPro, PII and PIII)

ジャンプ、コール、リターンは直前のジャンプ、コール、またはリターンの次の最初のクロックサイクルでは実行できない。それ故、連続したジャンプは各々のジャンプに2クロックサイクル要すると思われ、プロセッサに並列動作を確実にさせるため、何か別の仕事をさせておくとよい。同じ理由により、ループ命令も1回当たり少なくとも2クロックサイクルかかる。

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

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

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

22.3 分岐を避ける (すべてのプロセッサ)

ジャンプ、コール、リターンの数を減らしたい理由が多くありうる。

コールとリターンはインライン・マクロによる小さな手続きによって置き換えることにより避けられる。そして多くの場合分岐の数はコードを再構築することによって減らすことができる。例えば、ジャンプへのジャンプは最後の目標へのジャンプに置き換えるべきである。ある場合では、条件が同じかもしくは判明している時、条件分岐でさえ置き換えは可能である。リターンへのジャンプはリターンに置き換えられる。リターンへのリターンを除去したいならば、スタックポインタを操作すべきではない。というのは、リターン・スタック・バッファ(ROB)の予測機構の動作を妨げるからである。その代わりに、ジャンプ命令で先読みしたコールに置き換えればよい。例えば、CALL PRO1 / RETは、PRO1がRETと同じ種類の命令で終わっていれば、JMP PRO1に置き換えられる。

また、ジャンプされるコードを複写してジャンプを減らしてもよい。これは、リターンする前に二通りのジャンプがループ中にある場合に便利である。例:

A:      CMP     [EAX+4*EDX],ECX
        JE      B
        CALL    X
        JMP     C
B:      CALL    Y
C:      INC     EDX
        JNZ     A
        MOV     ESP, EBP
        POP     EBP
        RET
Cへのジャンプはループの最後に複写することにより削除できる。
A:      CMP [EAX+4*EDX],ECX
        JE      B
        CALL    X
        INC     EDX
        JNZ     A
        JMP     D
B:      CALL    Y
C:      INC     EDX
        JNZ     A
D:      MOV     ESP, EBP
        POP     EBP
        RET
最もしばしば実行される分岐はここでは最初に置くべきである。Dへのジャンプはループの外側にあり、それゆえあまり重大でない。この分岐がさらにしばしば行われるようであれば、DへのジャンプをD以降の三行で置き換えることにより同様に最適化できる。

22.4 フラグを用いた条件分岐の回避 (すべてのプロセッサ)

最も重要な除去すべきジャンプは、条件分岐である。予測が当たりにくい時は特にである。時には、分岐と同じ効果をビットとフラグの巧妙な操作で得られる。例えば、符号つき数の絶対値を分岐なしで計算できる。
        CDQ
        XOR EAX,EDX
        SUB EAX,EDX
(PPlainとPMMXでは、CDQの代わりに 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
任意の条件ですべてのビットをセットする:  XOR EAX,EAX / SETNcond AL / DEC EAX
(最後の例では、条件を反転するのを忘れないように)

この例は、二つの符号なし数の小さいほうを見つける: 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

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

22.3 条件分岐を条件移動命令で置き換える (PPro, PII and PIII)

PPro、PIIとPIIIプロセッサは、特に分岐を避けることを意図した、条件つきMOV命令を持っている。というのは、これらのプロセッサでは分岐予測ミスは大変時間を消費するからである。整数と浮動小数点の両方に条件移動命令がある。これらのプロセッサでだけ走るようなコードでは、予測のうまくいかない分岐は可能ならすべて条件つきMOVで置き換えるべきである。すべてのプロセッサで走らせたければ、最も重要な部分の二つのバージョンを作るとよい。一つは条件移動命令を備えたプロセッサ用、もう一つはそれ以外のプロセッサ用(条件移動命令を備えているかどうかの判定方法については27章10を見よ)である。

分岐予測ミスのペナルティはたいへん高いため、いくつかの余計な命令が必要でもそれを条件移動命令で置き換える方が有利である。しかし条件移動命令は長い依存の連鎖を発生するという不利な点がある。条件移動は三つのオペランドの準備ができるまで待機する。それは状態フラグと二つの移動オペランドである。これら三つのオペランドに依存の連鎖やキャッシュ・ミスによる遅延が生じやすくないかよく考える必要がある。状態フラグが移動オペランドよりずっと早く準備ができるなら、同じく分岐を用いてもよい。というのは、分岐予測ミスの可能性は移動オペランドを待っている間に解決されるからである。結局必要とされない移動オペランドのために長く待機しなければならない状況では、条件分岐の方が分岐予測ミスのペナルティにも関わらず速いであろう。この正反対の状況とは、移動オペランドの準備が早くできて、まだ状態フラグが遅延されている状況である。この状況においては、分岐予測ミスが起きがちであれば条件移動の方が望ましい。


23. コードサイズの縮小 (すべてのプロセッサ)

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

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

ジャンプの番地、データの番地、データ定数はどれも、符号拡張されるバイトで表現できるなら、つまり、-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バイト
    そして
    MOV EAX,-1              ; 5バイト
    は次のように変えるとよい:
    OR EAX,-1               ; 3バイト

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

        MOV     [mem1],200      ; 10バイト
        MOV     [mem2],200      ; 10バイト
        MOV     [mem3],201      ; 10バイト
        MOV     EAX,100         ;  5バイト
        MOV     EBX,150         ;  5バイト

mem1とmem3は両方ともmem2から-128/+127の範囲内にあると仮定すれば、これは次のように変更するとよい:

        MOV     EBX, OFFSET mem2       ;  5バイト
        MOV     EAX,200                ;  5バイト
        MOV     [EBX+mem1-mem2],EAX    ;  3バイト
        MOV     [EBX],EAX              ;  2バイト
        INC     EAX                    ;  1バイト
        MOV     [EBX+mem3-mem2],EAX    ;  3バイト
        SUB     EAX,101                ;  3バイト
        LEA     EBX,[EAX+50]           ;  3バイト

LEA命令のAGIストールに気をつけてほしい(PPlainとPMMX)。

違う命令は違う長さを持つことを考慮するのもよい。次の命令は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をベース・ポインタとして使うのに比べて1バイト短くなる(ESPを除く)。例:

    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]  と同じサイズ

スケールド・インデックス・ポインタを持ち、ベース・ポインタを持たない命令は、たとえ変位がゼロであっても、4バイトの変位が必要である。

    LEA EAX,[EBX+EBX]  は LEA EAX,[2*EBX] より小さい


24. 浮動小数点コードのスケジューリング (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クロックサイクルかかり、オーバーラップ可能である。そのため、これらの命令は上に示した方法でスケジューリングできる。メモリオペランドが一次キャッシュにあって適切にアラインされていれば、メモリオペランドはレジスタオペランドより時間がかかることはない。

これまでにあなたは、例外のある規則に慣れてきたに違いない。そして、オーバーラップの規則も例外ではない。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が終わるのを待たなければならない。

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

    FDIV    QWORD PTR [EBX]
    CMP     [ESI],EAX
    FMUL    QWORD PTR [ESI]

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

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

算術演算ユニットはパイプライン中で読み込みユニットの一つ後のステップにあるので、浮動小数点命令にメモリオペランドを使うことに対するペナルティはない。これのトレードオフは浮動小数点データをメモリに格納するときに現れる。メモリへの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クロックオーバーラップして実行される。


25. ループの最適化 (すべてのプロセッサ)

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

後に続くすべての例は、すべてのデータがL1キャッシュにあると仮定している。速度がキャッシュ・ミスにより制限されるときは命令を最適化する理由はない。それどころか、多少ともキャッシュ・ミスを少なくするためにデータをまとめて集中化させるべきである(7章を参照)。

25.1 ループの最適化 (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.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
これはうまい答えのように見えるが、遅くてペアにできない命令を含んでいるので、最適ではない。すべてのデータが一次キャッシュにある場合、繰り返し当たり11クロックサイクルかかる。

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

例1.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命令を分割することなく同じ速度を得ることはできただろうが、他のペアにできない命令は分割が必要である。

カウンタと添字に同じレジスタを使う (PPlain and PMMX)

例1.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で終わりにする (PPlain and PMMX)

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

例1.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を起こすだろうから、もっと賢くやらなければならない。

例1.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を起こすだろうから最適解ではない。 INC命令のキャリーフラグを変化させないトリックはPPlainとPMMXで有効であるが、PPro、PIIとPIIIではパーシャル・フラグ・ストールの原因となる。

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

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

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

例1.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 の間に置く。

ループを伸ばす (PPlain and PMMX)

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

例1.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)

例1.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回を超えて伸ばす (PPlain and PMMX)

操作当たりのループのオーバーヘッドを減らすために、繰り返し当たり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を加える。

例1.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 命令は、例1.4のようにループカウンタを添字に使うようにすれば避けることができただろう。しかしながら、これだとループ本体の命令数が奇数になるので、ペアにならない命令ができてループは依然として5クロックかかるだろう。分岐命令をペアにしないと、分岐が予測ミスしたときに最後の操作の後で1クロック節約できるが、配列の最後へのポインタを設定し、-Nを計算するための準備のコードで余分なクロックサイクルを消費しなければならないため、二つの方法はちょうど同じ速さになる。ここで示した方法は最も簡単で最も短い。

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

例1.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命令の利点を生かすことができる。

例1.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ストールが起きる。

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

例1.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命令を使うことには高いペナルティがあるので、例1.9のように32ビットレジスタを使いたい状況は依然としてあるだろう。

浮動小数点命令を含むループ (PPlain and PMMX)

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

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

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

例1.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:
ここでは例1.6と同じ方法―ループカウンタを添字のレジスタとして使い、負の値から0まで数える方法―を使っている。また、ある操作の最後は次の最初とオーバーラップする。

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

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

浮動小数点ループを伸ばす (PPlain and PMMX)

3回伸ばしたDAXPYループはたいそうこみいっている。

例1.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は、次の実行の最初の命令である。ここでは、例1.9と例1.10のように、配列の終わりを読み過ぎて、最後に余分な値を捨てるという解答をするのは、たいへん魅力的だろう。しかし、配列の後のメモリ位置が正当な浮動小数点数を含んでいない場合、余分な値を読むことで、デノーマルオペランド例外が発生するため、浮動小数点ループでは、これは勧められない。これを避けるためには、少なくとももう一つの操作をメインループの後でしなければならない。

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

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

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

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

例1.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回伸ばすべきだと言っておこう。

25.2 ループの最適化 (PPro, PII and PIII)

前の節(25章1)の中で、私はいかにしてPPlainとPMMXにおいてペアリングを改善するための繰り返しとループのアンロールを使うかを説明した。PPro、PIIとPIIIにおいては、アウト・オブ・オーダー実行機構のおかげで、これを行う理由がない。しかし、気を付けなければならない別の全く難しい問題がある。最も重要なものはifetch境界とレジスタ・リード・ストールである。

私は以前のマイクロプロセッサのための25章1と同じ例を選んだ。それは整数を配列から読み、整数の符号を変えて、他の配列に入れる手続きである。

この手続きをC言語で書けば次のようになろう。

void ChangeSign (int * A, int * B, int N) {
  int i;
  for (i=0; i<N; i++) B[i] = -A[i];}
アセンブリ言語に翻訳すれば、これは次のように書けるだろう。

例2.1

_ChangeSign PROC 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
_ChangeSign     ENDP
これはよい解答に見えるが、最適ではない。というのは、μ-OPSを多く生成する、あまりよくない命令LOOP, LODSDとSTOSDを使っているからである。これはすべてのデータがL1キャッシュにあるとすれば、一回当たり6〜7クロックを必要とする。これを避けると次のようになる。

例2.2

        MOV     ECX, [N]
        JECXZ   L2
        MOV     ESI, [A]
        MOV     EDI, [B]
ALIGN   16
L1:     MOV     EAX, [ESI]       ; len=2, p2rESIwEAX
        ADD     ESI, 4           ; len=3, p01rwESIwF
        NEG     EAX              ; len=2, p01rwEAXwF
        MOV     [EDI], EAX       ; len=2, p4rEAX, p3rEDI
        ADD     EDI, 4           ; len=3, p01rwEDIwF
        DEC     ECX              ; len=1, p01rwECXwF
        JNZ     L1               ; len=2, p1rF
L2:
注釈は以下のように解釈して欲しい。MOV EAX,[ESI]命令は2バイト長で、ESIから読み出してEAXに書き込む(リネームする)ポート2のための一つのμ-OPSを生成する。この情報はボトルネックの可能性を解析するのに必要である。

まず最初に、命令の解釈(14章)を解析しよう。命令の一つは二つのμ-OPSを生成する(MOV [EDI],EAX)。この命令はデコーダD0に行かなければならない。ループ中には三つのデコード・グループがあるのでこれは3クロックサイクルでデコードできる。

次に、命令の取り込み(15章)を見てみよう。ifetch境界が最初の三つの命令のデコードの妨げになるならば、次の繰り返しでifetchブロックが必要な最初の命令で始まるように三つのデコード・グループがifetchブロックの最後にあるようにするだろう。そして遅延は最初の回だけで済む。より悪い状況は16バイト境界とifetch境界が最後の三つの命令のどこかにある場合である。ifetch表に従えば、これは1クロックの遅延を生じ、次の繰り返しで最初の16バイトでアラインされたifetchブロックを持つ原因になり、問題が繰り返しの最中ずっと続くことになる。この結果として、取り込み時間は一回当たり3ではなく4になる。この状況を防ぐ二つの方法がある。最初の方法は、最初のifetchブロックがまたぐ場所を調整することである。もう一つの方法は、16バイト境界を調整することである。後者の方法が最も簡単である。ループの全体が15バイトしかないので、以前に述べた方法でループの入り口を16バイトでアラインすれば、16バイト境界を避けることができる。これはループ全体を一つのifetchブロックに置くので、これ以上の命令取り込みの解析は不要である。

三番目にレジスタ・リード・ストール(16章)の問題を見てみよう。このループにあるレジスタは、少なくとも2クロックサイクル前に書き込みされているので、レジスタ・リード・ストールはない。

四番目の解析は実行(18章)である。ループ中にあるμ-OPSの数は3で割り切れないので、ジャンプ命令が最初にスロットからリタイアしなければならない時はリタイアメント・スロットは最適には使われないだろう。リタイアメントに必要な時間はμ-OPSを3で割った数を切り上げた整数になる。

結論として、このループは16バイト境界でアラインされていれば、一回当たり3クロックで実行できる。条件分岐はループを抜ける時を除いて予測が毎回当たるものと仮定する(22章2)。

カウンタと添字に同じレジスタを使い、カウンタを0で終わらせる (PPro, PII and PIII)

例2.3

        MOV     ECX, [N]
        MOV     ESI, [A]
        MOV     EDI, [B]
        LEA     ESI, [ESI+4*ECX]          ; 配列Aの最後を指す
        LEA     EDI, [EDI+4*ECX]          ; 配列Bの最後を指す
        NEG     ECX                       ; -N
        JZ      SHORT L2
ALIGN   16
L1:     MOV     EAX, [ESI+4*ECX]          ; len=3, p2rESIrECXwEAX
        NEG     EAX                       ; len=2, p01rwEAXwF
        MOV     [EDI+4*ECX], EAX          ; len=3, p4rEAX, p3rEDIrECX
        INC     ECX                       ; len=1, p01rwECXwF
        JNZ     L1                        ; len=2, p1rF
L2:
ここではカウンタと添え字に同じレジスタを用いることによって、μ-OPSの数を六つに減らした。添え字を負の値から0まで数え上げるために、ベース・ポインタは配列の最後を指している。

デコードについて。ループ中には二つのデコード・グループがあるのでデコードには2クロックかかる。

命令取り込みについて。ループは少なくとも16バイトの数よりも1クロックサイクル余計にかかる。この場合はコードが11バイトしかないので、すべてを1つのifetchブロックに入れることが可能である。ループの入り口を16バイト境界にアラインすることによって16バイトブロックが1つを超えないようにし、命令の取り込みを2クロックで可能にした。

レジスタ・リード・ストールについて。ESIとEDIはループ中では読み出されるが、変化しない。従ってこれは常置レジスタからの読み出しと数えられる。しかし同じ三つ組にはない。レジスタEAX, ECXとフラグはループ中で変化させられ、書き戻される前に読まれる。従って常置レジスタからの読み出しにはならない。結論として、レジスタ・リード・ストールは存在しない。

実行について。

port 0 または 1: 2μ-OPS
port 1: 1μ-OPS
port 2: 1μ-OPS
port 3: 1μ-OPS
port 4: 1μ-OPS

従って実行時間は1.5クロックである。

リタイアメントについて。6μ-OPSだから2クロックである。

結論は、このループは一回当たり2クロックサイクルかかる。

ESIとEDIの代わりに絶対番地を用いれば、ループは3クロックかかる。というのは、一つの16バイト・ブロックに収まらなくなるからである。

ループのアンロール (PPro, PII and PIII)

ループの実行時に二つ以上の操作を行い、同時に実行回数を減らすことを、ループのアンロールと呼ぶ。以前のプロセッサにおいては、ペアリングにより並列動作を得るため(
25章1)にループのアンロールをしたと思う。これはPPro, PIIとPIIIではアウト・オブ・オーダー実行機構がこの面倒を見てくれるので必要ではない。二つの異なるレジスタを使用する必要もない。これはレジスタ・リネーミングが面倒を見てくれる。ここでのループのアンロールの目的は、繰り返し時のループのオーバーヘッドを減らすことにある。

以下の例は例2.2と同じであるが、2でアンロールしてある。これが意味するのは、繰り返し毎に二つの操作を行い、回数を半分にするということである。

例2.4

        MOV     ECX, [N]
        MOV     ESI, [A]
        MOV     EDI, [B]
        SHR     ECX, 1           ; N/2
        JNC     SHORT L1         ; Nが奇数かどうか調べる
        MOV     EAX, [ESI]       ; 奇数なら最初に一度実行する
        ADD     ESI, 4
        NEG     EAX   
        MOV     [EDI], EAX 
        ADD     EDI, 4     
L1:     JECXZ   L3

ALIGN   16
L2:     MOV     EAX, [ESI]       ; len=2, p2rESIwEAX
        NEG     EAX              ; len=2, p01rwEAXwF
        MOV     [EDI], EAX       ; len=2, p4rEAX, p3rEDI
        MOV     EAX, [ESI+4]     ; len=3, p2rESIwEAX
        NEG     EAX              ; len=2, p01rwEAXwF
        MOV     [EDI+4], EAX     ; len=3, p4rEAX, p3rEDI
        ADD     ESI, 8           ; len=3, p01rwESIwF
        ADD     EDI, 8           ; len=3, p01rwEDIwF
        DEC     ECX              ; len=1, p01rwECXwF
        JNZ     L2               ; len=2, p1rF
L3:
例2.2において、ループのオーバーヘッド(すなわちポインタとカウンタを調整し、ジャンプして戻る)は4μ-OPSで、本当の仕事は4μ-OPSである。2でアンロールすれば、本当の仕事は倍になり、オーバーヘッドは一回なので、全部で12μ-OPSになる。これはμ-OPSにおいて50%から33%へオーバーヘッドを減らす。アンロールされたループは偶数回の操作しかできないので、Nが奇数かどうかを調べ、奇数ならループの外側で一回実行している。

このループの命令の取り込みを分析すると、新しいifetchブロックはADD ESI,8命令で始まり、これはD0デコーダへと送られる。これでは、ループのデコードに5クロックサイクルかかり、我々が望む4クロックサイクルではなくなる。この問題は、先の命令を'長い'バージョンで書くことにより解決できる。MOV [EDI+4],EAXを

    MOV [EDI+9999],EAX     ; 長い変位の命令を作る
    ORG $-4
    DD 4                   ; 変位を4で上書きする
これは新しいifetchブロックを'長い'MOV [EDI+4],EAX命令で始まるように強いるので、デコード時間は今や4クロックに減る。パイプラインの残りは1クロック当たり三つのμ-OPSを扱えるので、予測される実行時間は一回当たり4クロックまたは2クロックである。

この解を実行すると、実際はもう少し余計にかかる。私の測定によれば、一回当たりおよそ4.5クロックかかっている。これは多分μ-OPSの並び替えが最適化されてないことによるものだろう。おそらく、ROBはμ-OPSのための最適な実行順序を見つけず、最善でない順序で実行している。この問題は予測できなく、実験だけがこの問題を明らかにできる。ROBの並べ替えの振る舞いをいくらか手動で補助することができる。

例2.5

ALIGN   16
L2:     MOV     EAX, [ESI]       ; len=2, p2rESIwEAX
        MOV     EBX, [ESI+4]     ; len=3, p2rESIwEBX
        NEG     EAX              ; len=2, p01rwEAXwF
        MOV     [EDI], EAX       ; len=2, p4rEAX, p3rEDI
        ADD     ESI, 8           ; len=3, p01rwESIwF
        NEG     EBX              ; len=2, p01rwEBXwF
        MOV     [EDI+4], EBX     ; len=3, p4rEBX, p3rEDI
        ADD     EDI, 8           ; len=3, p01rwEDIwF
        DEC     ECX              ; len=1, p01rwECXwF
        JNZ     L2               ; len=2, p1rF
L3:
今このループは毎回4クロックで実行される。これは命令取込みブロック(ifetchブロック)の問題をも解決している。必要な代償は余計なレジスタを必要とすることである。というのはレジスタ・リネーミングの恩恵にあずかれないからである。

3以上のアンロール

ループのアンロールは、ループのオーバーヘッドが実行時間中で高い比率を占める場合に望ましい。例2.3でオーバーヘッドはほんの2μ-OPSであるため、アンロールによる利得はほとんどない。しかし、それでも練習のために、その方法を示そうと思う。

本当の仕事は4μ-OPSで、オーバーヘッドは2である。2でアンロールすれば2×4+2=10μ-OPSを得る。リタイア時間は10/3になるはずなので、整数に切り上げて、4クロックサイクルとする。この計算は2でアンロールしても得るものが何もないことを示している。4でアンロールすると、次のようになる。

例2.6

        MOV     ECX, [N]
        SHL     ECX, 2                    ; 扱うバイト数
        MOV     ESI, [A]
        MOV     EDI, [B]
        ADD     ESI, ECX                  ; 配列Aの最後を指す
        ADD     EDI, ECX                  ; 配列Bの最後を指す
        NEG     ECX                       ; -4*N
        TEST    ECX, 4                    ; Nが奇数かどうか調べる
        JZ      SHORT L1
        MOV     EAX, [ESI+ECX]            ; Nが奇数なので、一度実行する
        NEG     EAX
        MOV     [EDI+ECX], EAX
        ADD     ECX, 4
L1:     TEST    ECX, 8                    ; N/2が奇数かどうか調べる
        JZ      SHORT L2
        MOV     EAX, [ESI+ECX]            ; N/2が奇数なので、二度余計に実行する
        NEG     EAX
        MOV     [EDI+ECX], EAX
        MOV     EAX, [ESI+ECX+4]
        NEG     EAX
        MOV     [EDI+ECX+4], EAX
        ADD     ECX, 8
L2:     JECXZ   SHORT L4

ALIGN   16
L3:     MOV     EAX, [ESI+ECX]            ; len=3, p2rESIrECXwEAX
        NEG     EAX                       ; len=2, p01rwEAXwF
        MOV     [EDI+ECX], EAX            ; len=3, p4rEAX, p3rEDIrECX
        MOV     EAX, [ESI+ECX+4]          ; len=4, p2rESIrECXwEAX
        NEG     EAX                       ; len=2, p01rwEAXwF
        MOV     [EDI+ECX+4], EAX          ; len=4, p4rEAX, p3rEDIrECX
        MOV     EAX, [ESI+ECX+8]          ; len=4, p2rESIrECXwEAX
        MOV     EBX, [ESI+ECX+12]         ; len=4, p2rESIrECXwEAX
        NEG     EAX                       ; len=2, p01rwEAXwF
        MOV     [EDI+ECX+8], EAX          ; len=4, p4rEAX, p3rEDIrECX
        NEG     EBX                       ; len=2, p01rwEAXwF
        MOV     [EDI+ECX+12], EBX         ; len=4, p4rEAX, p3rEDIrECX
        ADD     ECX, 16                   ; len=3, p01rwECXwF
        JS      L3                        ; len=2, p1rF
L4:
ifetchブロックは望む場所にある。デコード時間は6クロックである。

ここではレジスタ・リード・ストールが問題となる。というのは、ECXはループの最後付近でリタイアし、ESI, EDIとECXすべてを読み出すからである。レジスタ・リード・ストールを避けるために、ESIが最後で読み出されないよう命令を並び替えた。他の言い方で言えば、命令の並び替えと余計なレジスタ(EBX)の使用の理由は、以前の例と違うのである。

12μ-OPSあり、ループは毎回6クロックで実行される、または一回の操作で1.5クロック要する。

最大の速度を得るために、高い係数でアンロールしたい誘惑にかられるかもしれない。しかし、ほとんどの場合ループのオーバーヘッドはループ中で毎回1クロックサイクル程度は減らすことができるので、2でアンロールすることに比べ4でアンロールしても、操作一回当たり1/4クロックサイクル節約できるだけで、ほとんど努力に値しない。ループのオーバーヘッドがループ内部の処理に比べて高く、Nが非常に大きい時にのみ、4でアンロールすることを考えるべきだろう。4を超えるアンロールは意味がない。

過度なループのアンロールの欠点は

  1. NをRで割った余りを計算する必要がある。ここでRはアンロールの係数、そしてNをRで割った余りを求める操作をメインループの前または後でしなければならない。これは、Rで割った余りの操作の回数を求めるために必要である。
  2. コード片は初めて実行される時は普通より多くの時間を必要とし、初めての実行のペナルティは、コードの中で最も大きい。特にNが小さい時はそうである。
  3. 過度のコードサイズはコードキャッシュの利用を非効果的にする。

アンロールの係数に2のべき乗でない数を使うと、NをRで割った余りを求める大変難しい計算が必要になり、一般的にNがRで割り切れない数を使うことは勧められない。 例1.14に3でアンロールする方法を示す。

32ビットレジスタ中で8または16ビットのオペランドを同時に扱う (PPro, PII and PIII)

同じ32ビットレジスタの中で一度に4バイトを扱う可能性が時々ある。次の例はバイト配列のすべての要素に2を加算する。

例2.7

        MOV     ESI, [A]         ; バイト配列の番地
        MOV     ECX, [N]         ; バイト配列の要素数
        JECXZ   L2
ALIGN   16
        DB   7  DUP (90H)        ; アラインメントの調整用の七つのNOP

 L1:    MOV     EAX, [ESI]       ; 4バイトの読み出し
        MOV     EBX, EAX         ; EBXにコピーする
        AND     EAX, 7F7F7F7FH   ; EAXの各々のバイトの下位7ビットを得る
        XOR     EBX, EAX         ; 各々のバイトの上位1ビットを得る
        ADD     EAX, 02020202H   ; 四つの全てのバイトに望まれる値を加算する
        XOR     EBX, EAX         ; ビットを再び結合する
        MOV     [ESI], EBX       ; 結果を書き込む
        ADD     ESI, 4           ; ポインタを増やす
        SUB     ECX, 4           ; ループカウンタを減らす
        JA      L1               ; ループする
L2:
各々のバイトの上位ビットを、加算時にあるバイトから次のバイトへとキャリー(桁上げ)が生じるのを防ぐためにマスクしたのに注意して欲しい。最上位ビットを再び書き込むために、ADDよりもXORを用いた。配列はもちろん4でアラインされていなければならない。

このループは理想的には毎回4クロックかかるはずだが、依存の連鎖と難しいリオーダのためにいくぶん余計にかかる。PIIとPIIIではMMXレジスタを使用すれば同じことをより効率的に行えるだろう。

次の例はゼロで終わる文字列を、最初のバイトがゼロであることを探すことによって見つけるものである。これはREPNE SCASBを使うよりも速い。

_strlen PROC    NEAR
        PUSH    EBX
        MOV     EAX,[ESP+8]            ; 文字列のポインタを得る
        LEA     EDX,[EAX+3]            ; 最後に使われるポインタ+3
L1:     MOV     EBX,[EAX]              ; 最初の4バイトを読み出す
        ADD     EAX,4                  ; ポインタを増やす
        LEA     ECX,[EBX-01010101H]    ; 各々のバイトから1を減算する
        NOT     EBX                    ; すべてのバイトを反転する
        AND     ECX,EBX                ; これら二つのANDを取る
        AND     ECX,80808080H          ; すべての符号を調べる
        JZ      L1                     ; ゼロのバイトがなければループを続ける
        MOV     EBX,ECX
        SHR     EBX,16
        TEST    ECX,00008080H          ; 最初の2バイトを調べる
        CMOVZ   ECX,EBX                ; 最初の2バイトがゼロでなければ、右シフトする
        LEA     EBX,[EAX+2]
        CMOVZ   EAX,EBX
        SHL     CL,1                   ; 分岐を避けるためにキャリーフラグを使う
        SBB     EAX,EDX                ; 長さを計算する
        POP     EBX
        RET
_strlen ENDP

このループは毎回4バイトを調べ、3クロックずつかかる。文字列はもちろん4でアラインされていなければならない。コードは文字列の最後を超えて読むかもしれないので、文字列はセグメントの最後に置くべきでない。

4バイトを同時に扱うことは大変難しくなりがちである。このコードはバイトがゼロの時、そしてその時のみ、ゼロでない値を生成する式を使っている。これは4バイトを一度の操作で調べられるようにする。このアルゴリズムはすべてのバイトから1を減算することを伴う(LEA ECX命令中で)。前の例2.7のようには、各々のバイトの最上位ビットを減算前にマスクはしていない。それで減算は次のバイトからのボロー(借り)を生成することがあるが、それはゼロの時のみで、正確には次のバイトが何であるかを気にする必要がない状況である。というのは前方へ最初のゼロを探しているからである。もし後方へ探すとすれば、ゼロを検出してからダブル・ワードの再読込みをし、4バイトすべてから最後のゼロを探すか、もしくはBSWAPを用いてバイトの並びを逆にしなければならない。ゼロでないバイト値を探したいならば、すべての4バイトを探したい値でXORを取り、上記の方法でゼロを探せばよい。

MMX命令を含むループ (PII and PIII)

MMX命令を使えば一操作で8バイトを比較することができる。

例2.9

_strlen PROC    NEAR
        PUSH    EBX
        MOV     EAX,[ESP+8]
        LEA     EDX,[EAX+7]
        PXOR    MM0,MM0
L1:     MOVQ    MM1,[EAX]        ; len=3 p2rEAXwMM1
        ADD     EAX,8            ; len=3 p01rEAX
        PCMPEQB MM1,MM0          ; len=3 p01rMM0rMM1
        MOVD    EBX,MM1          ; len=3 p01rMM1wEBX
        PSRLQ   MM1,32           ; len=4 p1rMM1
        MOVD    ECX,MM1          ; len=3 p01rMM1wECX
        OR      ECX,EBX          ; len=2 p01rECXrEBXwF
        JZ      L1               ; len=2 p1rF
        MOVD    ECX,MM1
        TEST    EBX,EBX
        CMOVZ   EBX,ECX
        LEA     ECX,[EAX+4]
        CMOVZ   EAX,ECX
        MOV     ECX,EBX
        SHR     ECX,16
        TEST    BX,BX
        CMOVZ   EBX,ECX
        LEA     ECX,[EAX+2]
        CMOVZ   EAX,ECX
        SHR     BL,1
        SBB     EAX,EDX
        EMMS
        POP     EBX
        RET
_strlen ENDP
このループはポート0, 1への七つのμ-OPSを持ち、その平均実行時間は毎回3.5クロックである。測定された時間は3.8クロックであり、6μ-OPS長の依存の連鎖があるにも関わらず、ROBはうまくこの状況を扱うことを示している。8バイトを4クロックよりも短い間に調べるということはREPNE SCASBよりも遙かに速い。

浮動小数点命令を含むループ (PPro, PII and PIII)

浮動小数点命令を含むループを最適化する手法は基本的には整数のループと同じであるが、命令実行の長い遅延による依存の連鎖に更に注意を注ぐべきである。

次のC言語のコードを考えよう。

  int i, n;  double * X;  double * Y;  double DA;
  for (i=0; i<n; i++)  Y[i] = Y[i] - DA * X[i];
このコード片(DAXPYと呼ばれる)は広く学習されている。というのはこれは線形方程式を解くかぎであるからである。

例2.10

DSIZE   = 8                      ; データサイズ (4 または 8)
        MOV     ECX, [N]         ; 要素数
        MOV     ESI, [X]         ; Xへのポインタ
        MOV     EDI, [Y]         ; Yへのポインタ
        JECXZ   L2               ; Nが0かどうか調べる
        FLD     DSIZE PTR [DA]   ; ループの外でDAを読み出す
ALIGN   16
        DB    2 DUP (90H)        ; アラインメントのための二つのNOP
L1:     FLD     DSIZE PTR [ESI]  ; len=3 p2rESIwST0
        ADD     ESI,DSIZE        ; len=3 p01rESI
        FMUL    ST,ST(1)         ; len=2 p0rST0rST1
        FSUBR   DSIZE PTR [EDI]  ; len=3 p2rEDI, p0rST0
        FSTP    DSIZE PTR [EDI]  ; len=3 p4rST0, p3rEDI
        ADD     EDI,DSIZE        ; len=3 p01rEDI
        DEC     ECX              ; len=1 p01rECXwF
        JNZ     L1               ; len=2 p1rF
        FSTP    ST               ; DAを捨てる 
L2:
依存の連鎖は10クロックサイクル長であるが、ループは毎回4クロックしかかからない。というのは新しい操作は以前の操作が終わってから始めるからである。アラインメントの目的は最後のifetchブロックにおいて16バイト境界を避けるためである。

例2.11

DSIZE   = 8                                ; データサイズ (4 または 8)
        MOV     ECX, [N]                   ; 要素数
        MOV     ESI, [X]                   ; Xへのポインタ
        MOV     EDI, [Y]                   ; Yへのポインタ
        LEA     ESI, [ESI+DSIZE*ECX]       ; 配列の最後へのポインタ
        LEA     EDI, [EDI+DSIZE*ECX]       ; 配列の最後へのポインタ
        NEG     ECX                        ; -N
        JZ      SHORT L2                   ; Nが0かどうか調べる
        FLD     DSIZE PTR [DA]             ; ループの外でDAを調べる
ALIGN   16
L1:     FLD     DSIZE PTR [ESI+DSIZE*ECX]  ; len=3 p2rESIrECXwST0
        FMUL    ST,ST(1)                   ; len=2 p0rST0rST1
        FSUBR   DSIZE PTR [EDI+DSIZE*ECX]  ; len=3 p2rEDIrECX, p0rST0
        FSTP    DSIZE PTR [EDI+DSIZE*ECX]  ; len=3 p4rST0, p3rEDIrECX
        INC     ECX                        ; len=1 p01rECXwF
        JNZ     L1                         ; len=2 p1rF
        FSTP    ST                         ; DAを捨てる
L2:
ここでは
例2.3と同じトリックを使っている。理想的にはこのループは3クロックのはずだが、測定するとおよそ3.5クロックである。これは長い依存の連鎖のためであると思われる。ループのアンロールはほとんど効果がない。

XMM命令を含むループ (PIII)

PIIIのXMM命令は四つの単精度浮動小数点数を並列に操作することができる。オペランドは16でアラインされていなければならない。

DAXPYのアルゴリズムはあまりXMM命令にふさわしくない。というのは精度が悪く、16でオペランドをアラインすることはできそうにないし、操作の種類が四つの乗算でなければ余計なコードが必要だからである。ここではそれでも、まさにXMM命令を含むループの例を示すためにコードを示そうと重う。

例2.12

        MOV     ECX, [N]                   ; 要素数
        MOV     ESI, [X]                   ; Xへのポインタ
        MOV     EDI, [Y]                   ; Yへのポインタ
        SHL     ECX, 2
        ADD     ESI, ECX                   ; Xの最後へのポインタ
        ADD     EDI, ECX                   ; Yの最後へのポインタ
        NEG     ECX                        ; -4*N
        MOV     EAX, [DA]                  ; ループの外でDAを読み出す
        XOR     EAX, 80000000H             ; DAの符号を反転する
        PUSH    EAX
        MOVSS   XMM1, [ESP]                ; -DA
        ADD     ESP, 4
        SHUFPS  XMM1, XMM1, 0              ; -DAをすべての四つのXMMレジスタにコピーする
        CMP     ECX, -16
        JG      L2
L1:     MOVAPS  XMM0, [ESI+ECX]            ; len=4 2*p2rESIrECXwXMM0
        ADD     ECX, 16                    ; len=3 p01rwECXwF
        MULPS   XMM0, XMM1                 ; len=3 2*p0rXMM0rXMM1
        CMP     ECX, -16                   ; len=3 p01rECXwF
        ADDPS   XMM0, [EDI+ECX-16]         ; len=5 2*p2rEDIrECX, 2*p1rXMM0
        MOVAPS  [EDI+ECX-16], XMM0         ; len=5 2*p4rXMM0, 2*p3rEDIrECX
        JNG     L1                         ; len=2 p1rF
L2:     JECXZ   L4                         ; 終了したかどうか調べる
        MOVAPS  XMM0, [ESI+ECX]            ; 1〜3個の操作がまだなら、さらに4回実行
        MULPS   XMM0, XMM1
        ADDPS   XMM0, [EDI+ECX]
        CMP     ECX, -8
        JG      L3
        MOVLPS  [EDI+ECX], XMM0            ; さらに2個の結果を書き込む
        ADD     ECX, 8
        MOVHLPS XMM0, XMM0
L3:     JECXZ   L4
        MOVSS   [EDI+ECX], XMM0            ; さらに1個の結果を書き込む
L4:
L1ループは四つの操作に5〜6クロックかかる。ECX命令はMULPS XMM0, XMM1命令の前後に置いた。これはXMM1レジスタの二つの部分がRAT中でESIとEDIと一緒に読み出すことによるレジスタ・リード・ストールを避けるためである。L2の後の余分なコードはNが4で割り切れない状況に対応する。このコードが配列AとBの最後を超えて読み出すことがあることに注意して欲しい。これは余計なメモリを読み出した時、その内容が普通の浮動小数点数でない時に最後の操作に遅延が生じる原因となる。できれば、ダミーの数値を置いて、操作回数が4で割り切れるようにし、L2の後の余分なコードを実行しないようにしなさい。


26. 問題となりやすい命令

26.1 XCHG (すべてのプロセッサ)

XCHG レジスタ,[メモリ]命令は危険である。デフォルトではこの命令は暗黙の内にLOCKプリフィックスがつけられる。これはキャッシュから使われるのを妨げる。この命令はそれゆえ大変時間を消費し、いつも避けるべきである。

26.2 キャリーフラグを通した回転命令 (すべてのプロセッサ)

1以外の数で回転するRCR、RCL命令は遅いので避けるべきである。

26.3 ストリング命令 (すべてのプロセッサ)

リピート・プリフィックスを使わないストリング命令は大変遅く、簡単な命令で置き換えるべきである。同じことがすべてのプロセッサでLOOP命令に、PPlainとPMMXでJECXZ命令に言える。

REP MOVSDとREP STOSDは繰り返し回数がさほど小さくなければ大変速い。できればいつもDWORDを使うようにし、転送元と転送先が8で確実にアラインされているようにしなさい。

データ転送の他の方法のいくつかはある状況ではより速い。詳しくは27章8を見なさい。

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

REP MOVSとREP STOSはPPro, PIIとPIIIではキャッシュライン全体を一度に移動する際に大変速く動く。これは、次の状態が重なった時に起きる。

これらの条件下では発行されるμ-OPSの数はREP MOVSDではおよそ215+2×ECX、REP STOSDではおよそ185+1.5×ECXである。速度は両方とも1クロックサイクル当たりおよそ5バイトで、これは上の条件が揃わない時の3倍の速度である。

バイト版とワード版もこの速い方法の恩恵にあずかることができるが、DWORD版よりも非効率的である。

REP STOSDは、REP MOVSDと同じ条件下では最適である。

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

26.4 ビットテスト命令(すべてのプロセッサ)

BT, BTC, BTRとBTS命令はなるべくTEST, AND, OR, XORに置き換えるべきである。PPlainとPMMXではシフト命令もよい。PPro, PIIとPIIIはメモリオペランドとのビットテストは避けるべきである。

26.5 整数乗算命令 (すべてのプロセッサ)

整数の乗算はPPlainとPMMXでは9クロックサイクル、PPro, PIIとPIIIでは4クロックサイクルかかる。そのため、定数による乗算を他の、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ビットオペランドしか使えない。

26.6 WAIT命令 (すべてのプロセッサ)

WAIT命令を省くことでしばしば、速度を上げることができる。WAIT命令は三つの働きがある。

a.古い8087プロセッサは、必ずコプロセッサが命令を受け取れるようにするため、すべての浮動小数点命令の前にWAIT命令を要求する。 b.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        ; 読まれる前に上書きするのを防ぐ
c.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が必要である。

26.7 FCOM命令 + FSTSW AX命令 (すべてのプロセッサ)

FNSTSW命令はすべてのプロセッサにおいて大変遅い命令である。PPro, PIIとPIIIプロセッサは遅いFNSTSWを避けるためのFCOMI命令を持っている。普通の並びFCOM / FNSTSW AX / SAHFの代わりにFCOMIを使えば、8クロックサイクル節約できる。それゆえ、可能な限りFNSTSWを避けるために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 は、PPlainとPMMXではペアにできないので使ってはいけない。

PPlainとPMMXでは、FNSTSW命令2クロックかかるが、浮動小数点命令の後では4クロック遅れる。おそらくステータスワードがパイプラインからリアイアするのを待っているからだろう。この遅延はステータスワードを変えられないFNOPの後でさえ起きる。整数命令の後ではそうではない。このFCOMからFNSTSWまでの遅延を整数命令で埋められるなら、整数命令とFNSTSW命令は4クロックしかかからない。FCOMの直後にペアにされたFXCHはFNSTSWを遅れさせない。不完全なペアリングの時でさえそうである。

    FCOM                  ; 1クロック
    FXCH                  ; 1〜2クロック (不完全なペアリング)
    INC DWORD PTR [EBX]   ; 3〜5クロック
    FNSTSW AX             ; 6〜7クロック

ここで、FTST命令はペアにできないので、FTST命令の代わりにFCOM命令を使いたいと思うかもしれない。FNSTSWの中にNが含まれていることを思い出して欲しい。FSTSW(N無し)は、さらに遅延を大きくするWAITプリフィックスを持っている。

浮動小数点値を比較するのに、時々整数命令を使うと速い場合がある。これは27章6に述べてある。

26.8 FPREM命令 (すべてのプロセッサ)

FPREM命令とFPREM1命令はすべてのプロセッサで遅い。これを次のようなアルゴリズムで置き換えても良い。まず逆数を掛け、整数へ切り捨てた数を減算することにより分数部分を得、次に除数を掛ける(整数へ切り捨てる方法は27章5を見よ)。

いくつかの説明書にはこれらの命令は不完全な剰余を与えるので、完全な剰余を得るためにFPREMまたはFPREM1を繰り返すことが必要だと書いてある。私は8087で始まるいくつかのプロセッサで試みたが、FPREMとFPREM1で反復が必要な状況は見つけられなかった。

26.9 FRNDINT命令 (すべてのプロセッサ)

この命令はすべてのプロセッサにおいて遅い。これは次のように置き換えよ。
    FISTP QWORD PTR [TEMP]
    FILD  QWORD PTR [TEMP]
このコードは[TEMP]への書き込みが終了する前に読み出そうと試みるためのペナルティの可能性があるにも関わらず、より速い。このペナルティを避けるために、他の命令を挟むことが望ましい。丸めの方法は
27章5を見よ。

26.10 FSCALE命令とべき乗関数 (すべてのプロセッサ)

FSCALE命令はすべてのプロセッサにおいて遅い。2を整数でべき乗するには、浮動小数点のべき乗の領域に必要なべき数を挿入する方が速く行える。2N、ここにNは符号付き整数だとすれば、これを求めるために、Nの範囲に合う以下の例を選択するとよい。

|N| < 27-1の場合は単精度を使える。

    MOV     EAX, [N]
    SHL     EAX, 23
    ADD     EAX, 3F800000H
    MOV     DWORD PTR [TEMP], EAX
    FLD     DWORD PTR [TEMP]

|N| < 210-1の場合は倍精度を使える。

    MOV     EAX, [N]
    SHL     EAX, 20
    ADD     EAX, 3FF00000H
    MOV     DWORD PTR [TEMP], 0
    MOV     DWORD PTR [TEMP+4], EAX
    FLD     QWORD PTR [TEMP]

|N| < 214-1の場合は倍々精度を使える。

    MOV     EAX, [N]
    ADD     EAX, 00003FFFH
    MOV     DWORD PTR [TEMP],   0
    MOV     DWORD PTR [TEMP+4], 80000000H
    MOV     DWORD PTR [TEMP+8], EAX
    FLD     TBYTE PTR [TEMP]

FSCALEはしばしばべき乗関数の計算に使われる。次のコードは遅いFRNDINT命令とFSCALE命令を用いないべき乗関数を示している。

; extern "C" long double _cdecl exp (double x);
_exp    PROC    NEAR
PUBLIC  _exp
        FLDL2E
        FLD     QWORD PTR [ESP+4]             ; x
        FMUL                                  ; z = x*log2(e)
        FIST    DWORD PTR [ESP+4]             ; round(z)
        SUB     ESP, 12
        MOV     DWORD PTR [ESP], 0
        MOV     DWORD PTR [ESP+4], 80000000H
        FISUB   DWORD PTR [ESP+16]            ; z - round(z)
        MOV     EAX, [ESP+16]
        ADD     EAX,3FFFH
        MOV     [ESP+8],EAX
        JLE     SHORT UNDERFLOW
        CMP     EAX,8000H
        JGE     SHORT OVERFLOW
        F2XM1
        FLD1
        FADD                                  ; 2^(z-round(z))
        FLD     TBYTE PTR [ESP]               ; 2^(round(z))
        ADD     ESP,12
        FMUL                                  ; 2^z = e^x
        RET

UNDERFLOW: 
        FSTP    ST
        FLDZ                                  ; 0を返す
        ADD     ESP,12        
        RET

OVERFLOW:
        PUSH    07F800000H                    ; 無限大
        FSTP    ST
        FLD     DWORD PTR [ESP]               ; 無限大を返す
        ADD     ESP,16
        RET

_exp    ENDP

26.11 FPTAN命令 (すべてのプロセッサ)

説明書によれば、FPTANは二つの値XとYを返し、プログラマーに結果を得るためにYをXで除算する仕事を残すとある。しかし、実際はXに常に1を返すため、除算をしなくてよい。私の試みによればすべての32ビットの浮動小数点ユニットを備えたインテル・プロセッサまたはコプロセッサでは、FPTANは、引数に関わらずいつもXに1を返した。すべてのプロセッサで間違いなくコードが走ることを確実にするには、Xが1かどうかを調べればよい。それはXで除算するよりも速い。Yの値は大変大きいが、決して無限大にはならない。それで、引数が有効な値ならば、Yが有効な値かどうか調べる必要はない。

26.12 FSQRT命令 (PIII)

PIIIにおいて平方根の近似値を速く計算するには、Xの平方根の逆数の近似値にXを乗算すればよい。
SQRT(x) = x * RSQRT(x)
RSQRTSS命令とRSQRTPS命令は12ビット精度の平方根の逆数の近似値を返す。インテルのアプリケーション・ノートのAP-803にあるNewton-Raphson法を使って精度を23ビットに改善できる。
x0 = RSQRTSS(a)
x1 = 0.5 * x0 * (3 - (a * x0)) * x0)
ここでx0は最初のaの平方根の逆数の近似値であり、x1はより良い近似値である。評価の順序は大切である。この式を平方根をえるために乗算する前に使わなければならない。

26.13 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版でも直っていない。

26.14 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  ; (結果はサインフラグ)
P>(このペアにできないことの原因はおそらく、この2バイト命令の最初のバイトが、何か他のペアにできない命令と同じで、プロセッサはペア可能性を決めるときに2番目のバイトもチェックする余裕がないことだろう。)

26.15 ビットスキャン命令 (PPlain and PMMX)

BSF命令とBSR命令はPPlainとPMMXにおいては、最も最適化が貧弱な命令である。実行にはおよそ11+2×nクロックサイクルかかる。ここで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は古い80287プロセッサとの互換性を保つためだけに必要
        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は古い80287プロセッサとの互換性を保つためだけに必要
        MOV     ECX, DWORD PTR [TEMP+4]
        SHR     ECX,20
        SUB     ECX,3FFH
        TEST    EAX,EAX       ; ゼロフラグのクリア
BS2:
これらのエミュレートコードはPPro, PII, PIIIでは用いるべきではない。というのはビットスキャン命令は1〜2クロックしかかからず、上で示したエミュレートコードは二つのパーシャル・メモリ・ストールがあるからである。

26.16 FLDCW命令 (PPro, PII and PIII)

PPro, PIIとPIIIはFLDCW命令の後で、コントロールワードを参照する浮動小数点命令(ほとんどの浮動小数点命令がそうである)を実行すると、深刻なストールが発生する。

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

コントロールワードを変えずに浮動小数点を整数に変換する方法については27章5を見なさい。


27. 特別な話題

27.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と書くことで、命令のサイズを縮小できる。後者のコードはPPlainとPMMXでAGIの遅れがない。もしたまたま値が0であるような(ループの後のループカウンタのような)レジスタがあれば、それをベースレジスタとして使ってコードサイズを縮小することもできる。

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

27.2 除算 (すべてのプロセッサ)

除算はたいそう時間を消費する。PPro, PIIとPIIIでは整数の除算はバイト、ワード、そしてダブルワードの除数ではそれぞれ19, 23, または39クロックかかる。PPlainとPMMXでは、符号無し整数の除算ではほぼ同じ、符号付き整数の除算ではいくらか余計にかかる。従って、たとえオペランドサイズプリフィックスのコストを払ってでも、オーバーフローを生成しないできるだけ最小のオペランドサイズを、そして可能なら符号なし除算を使うのがよい。

定数による除算 (すべてのプロセッサ)

2のべき数による符号なし除算は右へのシフトでできる。2Nによる符号無し除算は
        SHR     EAX, N
2Nによる符号付き整数の除算は
        CDQ
        AND     EDX, (1 SHL N) -1  ; または  SHR EDX, 32-N
        ADD     EAX, EDX
        SAR     EAX, N
SHRの選択は、N > 7の時はANDより短くなるが、ポート0(またはUパイプ)の実行ユニットへしか行けない。これに対しANDはポート0にもポート1にも(UパイプにもVパイプにも)行ける。

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

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

  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 = 2b)
  結果 = 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 = 234 / 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の最大値は少なくとも2r-bであり、時にはもっと大きい。コードが正しく動くxの最大値をちょうど知りたいなら、系統的なテストをしなければならない。

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

次の例はEAXを10で割って結果をEAXに返す。私はr=19の代わりにr=17を選んだ。たまたま最適化しやすいコードになり、xの同じ範囲をカバーするからである。f = 217 / 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 < 232, 0 < d < 232 についてDIV命令と同じ結果を与える。

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

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

もし除数が頻繁に変わって、SET_DIVISORに最適化が必要なら、BSR命令を26章15で与えたPPlainとPMMXプロセッサのためのコードで置き換えてもよい。

浮動小数点除算 (すべてのプロセッサ)

PPlainとPMMXでは、浮動小数点除算は、最高精度では39クロックサイクルかかる。浮動小数点コントロールワードで低い精度を指定することで、時間を節約できる(PPlainとPMMXでは、FDIVとFIDIVだけが低い精度で高速化し、他の命令はそうならないPPro, PIIとPIIIではFSQRTに同じことが適用される。他のどの命令を用いてもこれを高速化することはできない)。

除算の並列 (PPlain and PMMX)

PPlainとPMMXでは、浮動小数点除算と整数除算を並列に行って時間を節約することが可能である。PPro, PIIとPIIIでは不可能である。というのは、整数除算と浮動小数点除算は同じ回路を使うからである。 例: 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
(浮動小数点コントロールワードを望みの丸め方法に設定したか確かめよ)

高速除算のために逆数命令を使う (PIII)

PIIIでは除数に高速逆数命令RCPSSまたはRCPPSを用い、結果の逆数を乗算することができる。しかし、精度は12ビットしかない。はインテルのアプリケーション・ノートのAP-803にあるNewton-Raphson法を使って精度を23ビットに増大できる。
x0 = RCPSS(d)
x1 = x0 * (2 - d * x0) = 2*x0 - d * x0 * x0
ここでx0はdの除数としての逆数の最初の近似解であり、x1はより良い近似解である。この式は除数で乗算する前に使わなければならない。
        MOVAPS  XMM1, [DIVISORS]         ; 除数を読み出す
        RCPPS   XMM0, XMM1               ; 逆数の近似解
        MULPS   XMM1, XMM0               ; Newton-Raphson法
        MULPS   XMM1, XMM0
        ADDPS   XMM0, XMM0
        SUBPS   XMM0, XMM1
        MULPS   XMM0, [DIVIDENDS]        ; XMM0に解が求まる
これは四つの除算を23ビット精度で18クロックサイクルで行う。精度をさらに上げるにはNewton-Raphson法を浮動小数点レジスタで繰り返すことが可能であるが、あまり有利ではない。

この方法を整数の除算に用いたいなら、丸め誤差を調べなければならない。次のコードは四つの切り捨て除算をパックド形式の整数で行い、約42クロックサイクルかかる。これは、0 <= 被除数 < 7FFFFH かつ 0 < 除数 <= 7FFFFHの時に正確な結果を与える。

        MOVQ MM1, [DIVISORS]      ; 四つの除数を読み出す
        MOVQ MM2, [DIVIDENDS]     ; 四つの被除数を読み出す
        PUNPCKHWD MM4, MM1        ; 除数をDWORDにアンパックする
        PSRAD MM4, 16
        PUNPCKLWD MM3, MM1
        PSRAD MM3, 16
        CVTPI2PS XMM1, MM4        ; 除数を単精度浮動小数点数に変換(上位2オペランド)
        MOVLHPS XMM1, XMM1
        CVTPI2PS XMM1, MM3        ; 除数を単精度浮動小数点数に変換(下位2オペランド)
        PUNPCKHWD MM4, MM2        ; 被除数をDWORDにアンパックする
        PSRAD MM4, 16
        PUNPCKLWD MM3, MM2
        PSRAD MM3, 16
        CVTPI2PS XMM2, MM4        ; 被除数を単精度浮動小数点数に変換(上位2オペランド)
        MOVLHPS XMM2, XMM2
        CVTPI2PS XMM2, MM3        ; 被除数を単精度浮動小数点数に変換(下位2オペランド)
        RCPPS XMM0, XMM1          ; 除数の逆数の近似解
        MULPS XMM1, XMM0          ; Newton-Raphson法による精度向上
        PCMPEQW MM4, MM4          ; 合間に整数1を四つ作る
        PSRLW MM4, 15
        MULPS XMM1, XMM0
        ADDPS XMM0, XMM0
        SUBPS XMM0, XMM1          ; 23ビット精度の除数の逆数
        MULPS XMM0, XMM2          ; 被除数で乗算
        CVTTPS2PI MM0, XMM0       ; 下位二つの結果の切り捨て数
        MOVHLPS XMM0, XMM0
        CVTTPS2PI MM3, XMM0       ; 上位二つの結果の切り捨て数
        PACKSSDW MM0, MM3         ; 四つの結果をMM0にパックする
        MOVQ MM3, MM1             ; 結果を除数で乗算する
        PMULLW MM3, MM0           ; 丸め誤差をチェックする
        PADDSW MM0, MM4           ; 後の減算の補償として1を加算
        PADDSW MM3, MM1           ; 除数を加算。これは被除数より大きくなければならない
        PCMPGTW MM3, MM2          ; 小さすぎないか調べる
        PADDSW MM0, MM3           ; 小さすぎなければ1を加算
        MOVQ [QUOTIENTS], MM0     ; 四つの結果を書き込む
このコードは結果が小さすぎないかを調べ、そうであれば近似補正をしている。結果が大きすぎればこれは必要ない。

除算を避ける (すべてのプロセッサ)

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

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

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

27.3 浮動小数点レジスタの解放 (すべてのプロセッサ)

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

一つのレジスタを解放する最も速い方法は、FSTP ST である。二つのレジスタを解放する最も速い方法は、PPlainとPMMXにおいてはFCOMPPである。PPro, PIIとPIIIではFCOMPPまたは二回のFSTP STのどちらを使ってもよい。デコードの流れにぴったりくる方を選ぶ。

FFREEを使うことは勧められない。

27.4 浮動小数点とMMX命令との移行 (PMMX, PII and PIII)

浮動小数点コードが後に続く可能性がある時は、EMMS命令を最後のMMX命令の後に発行しなければならない。PMMXでは浮動小数点とMMX命令の切り替えに高いペナルティがある。 EMMS命令の後の最初の浮動小数点命令では58クロック余計に必要で、浮動小数点命令の後の最初のMMX命令ではおよそ38クロック余計に必要である。

PIIとPIIIではそのようなペナルティはない。EMMSの後の遅延は、最初の浮動小数点命令の前に整数命令を挟むことで隠せる。

27.5 浮動小数点数の整数への変換 (すべてのプロセッサ)

浮動小数点数を整数に変換する、またその逆は、メモリを経由しなければならない。
        FISTP   DWORD PTR [TEMP]
        MOV     EAX, [TEMP]
PPro, PIIとPIIIではこのコードは、[TEMP]への書き込みの前に[TEMP]から読み出そうとする試みにより、ペナルティがある。というのはFIST命令は遅いからである(
17章を見よ)。WAIT命令を挟んでも効果はない(26章6を見よ)。できるなら、ペナルティを避けるために[TEMP]への書き込みと[TEMP]からの読み出しの間に別の命令を挟むことが望ましい。これは後のすべての例に適用される。

C言語とC++言語の仕様のために、浮動小数点を丸めではなく切り捨てにより整数に変換することが求められる。大部分のCのライブラリで用いられている方法は、FISTP命令の前に浮動小数点のコントロールワードを切り捨てモードにし、後に再び丸めモードに戻す。この方法はすべてのプロセッサにおいて大変遅い。PPro, PIIとPIIIでは、浮動小数点のコントロールワードはリネームできないので、すべての続く浮動小数点命令はFLDCW命令がリタイアするのを待たなければならない。

CとC++において浮動小数点から整数に変換する際には常に、切り捨ての代わりに最も近い整数へ丸めることができるかどうか考えるべきである。標準ライブラリに高速な丸め関数がなければ、後に示す貴方専用のコードを作りなさい。

ループ内で切り捨てが必要で、残りの浮動小数点命令が切り捨てモードで正しく動くのならば、コントロールワードをループの外で切り替えるべきである。

コントロールワードを変えずに切り捨てを行う様々なトリックを使ってもよい。これは後の例に示した。これらの例はコントロールワードがデフォルトにセットされている、すなわち最も近い数への丸め、または偶数への丸めになっていることを想定している。

最も近い数、もしくは偶数への丸め

; extern "C" int round (double x);
_round  PROC    NEAR
PUBLIC  _round
        FLD     QWORD PTR [ESP+4]
        FISTP   DWORD PTR [ESP+4]
        MOV     EAX, DWORD PTR [ESP+4]
        RET
_round  ENDP

ゼロ方向への切り捨て

; extern "C" int truncate (double x);
_truncate PROC    NEAR
PUBLIC  _truncate
        FLD     QWORD PTR [ESP+4]   ; x
        SUB     ESP, 12             ; ローカル変数領域の確保
        FIST    DWORD PTR [ESP]     ; 丸められた値
        FST     DWORD PTR [ESP+4]   ; 浮動小数点値
        FISUB   DWORD PTR [ESP]     ; 丸められた値で減算
        FSTP    DWORD PTR [ESP+8]   ; 差
        POP     EAX                 ; 丸められた値
        POP     ECX                 ; 浮動小数点数
        POP     EDX                 ; 差(浮動小数点)
        TEST    ECX, ECX            ; xの符号を調べる
        JS      SHORT NEGATIVE
        ADD     EDX, 7FFFFFFFH      ; 差が < -0 の時にキャリーを生成する
        SBB     EAX, 0              ; x-round(x) < -0 の時に1を減算
        RET
NEGATIVE:
        XOR     ECX, ECX
        TEST    EDX, EDX
        SETG    CL                  ; 差が > 0 の時に1にする
        ADD     EAX, ECX            ; x-round(x) > 0 の時に1を加算
        RET
_truncate ENDP

負の無限大方向への切り捨て

; extern "C" int ifloor (double x);
_ifloor PROC    NEAR
PUBLIC  _ifloor
        FLD     QWORD PTR [ESP+4]   ; x
        SUB     ESP, 8              ; ローカル変数領域の確保
        FIST    DWORD PTR [ESP]     ; 丸められた値
        FISUB   DWORD PTR [ESP]     ; 丸められた値で減算
        FSTP    DWORD PTR [ESP+4]   ; 差
        POP     EAX                 ; 丸められた値
        POP     EDX                 ; 差(浮動小数点)
        ADD     EDX, 7FFFFFFFH      ; 差が < -0 の時にキャリーを生成
        SBB     EAX, 0              ; x-round(x) < -0 の時に1を減算
        RET
_ifloor ENDP

これらの手続きは -231 < x < 231-1 の時に動作する。これらはオーバーフローとNANはチェックしていない。

PIIIには単精度浮動小数点数の切り捨て命令がある(CVTTSS2SIとCVTTPS2PI)。これらの命令は単精度で十分ならば大変有用であるが、この切り捨て命令を使用するために、より高い精度の浮動小数点数を単精度に変換する際に、単精度への切り上げが行われる問題がある。

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]

「マジックナンバー」 251 + 252を加えることは、-231と+231の間のどんな整数も、倍精度浮動小数点数として格納されるときに下位32ビットに整合されるという効果がある。ゼロ方向に切り捨てるのを除くすべての丸め方法で、FISTPと結果は同じである。コントロールワードが切り捨てを指定しているときと、オーバーフローの場合は、結果はFISTPと異なる。古い80287プロセッサとの互換性のためにWAIT命令が必要かもしれない。26章6を参照せよ。

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

27.6 整数命令を使った浮動小数点演算 (すべてのプロセッサ)

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

浮動小数点値がゼロかどうかテスト

浮動小数点値のゼロは普通、32ビットまたは64ビットのゼロで表されるが、これには落とし穴がある。符号ビットがセットされているかもしれない! マイナスゼロは有効な浮動小数点数とみなされ、プロセッサは実際、例えば負の数とゼロを掛けると符号ビットがセットされたゼロを生成する。だから、浮動小数点数がゼロかどうかテストしたいなら、符号ビットをテストしないようにするべきである。例として
    FLD DWORD PTR [EBX] / FTST / FNSTSW AX / AND AH,40H / JNZ IsZero
代わりに整数命令を使って、符号ビットをシフトで追い出そう。
    MOV EAX,[EBX] / ADD EAX,EAX / JZ IsZero

浮動小数点数が倍精度(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を含む)でうまくいく。

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

整数の乗算 (PPlain and PMMX)

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

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

整数乗算は、PPro, PIIとPIIIでは浮動小数点乗算より速い。

整数除算 (PPlain and PMMX)

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

2進数を10進数に変換 (すべてのプロセッサ)

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

27.8 データブロックの移動 (すべてのプロセッサ)

データプロックを移動するいくつかの方法がある。最もありふれた方法はREP MOVSDであるが、ある状況下では他の方法が速い。

PPlainとPMMXでは浮動小数点命令は、転送先がキャッシュになければ、8バイトを一度に移動するにはより速い。

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においてのみ、そして転送先が一次キャッシュにないときのみ、有利であることに注意せよ。FLDとFSTP(Iを除く)を任意のビットパターンで使うことはできない。というのはデノーマル数を扱う際には遅くなり、あるビットパターンは変化しないという保証がないからである。

MMXプロセッサでは、転送先がキャッシュになければ、8バイトを一度に移動するにはMMX命令を使うほうが速い。

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

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

PPro, PIIとPIIIプロセッサでは、次の条件を満たしていればREP MOVSD命令がとりわけ速い(26章3を見よ)。

PIIでは、上の条件を満たしていなくて、転送先がL1キャッシュにありそうな場合は、MMXレジスタを用いた方が速い。ループは2でアンロールし、転送元と転送先はもちろん8でアラインされているべきである。

PIIIでは、上の条件を満たしてないか、転送先がL1キャッシュまたはL2キャッシュにある場合、データを移動する最も速い方法はMOVAPS命令を使うことである。

        SUB     EDI, ESI
TOP:    MOVAPS  XMM0, [ESI]
        MOVAPS  [ESI+EDI], XMM0
        ADD     ESI, 16
        DEC     ECX
        JNZ     TOP
FLDと違い、MOVAPSはどんなビットパターンも問題なく扱うことができる。転送元と転送先は16でアラインされてなければならないことを覚えておきなさい。

移動するデータのバイト数が16で割り切れない時は、16で割り切れる最も近い数に切り上げ、転送先のバッファに余分なスペースをいくらか開けておきなさい。これができなければ、残りのデータを他の方法で移動しなければならない。

PIIIではMOVNTQ命令またはMOVNTPS命令を使って、キャッシュを巻き添えにせずに直接RAMメモリに書き込む選択もある。これは、転送先がキャッシュに入って欲しくない時に役に立つ。MOVNTPSはMOVNTQよりもわずかに速い。

27.9 自己改変コード (すべてのプロセッサ)

コード片を書き換えた直後にそれを実行することによるペナルティは、PPlainで19クロック、PMMXで31クロック、PPro, PIIとPIIIでは150〜300クロックになる。80486以前のプロセッサでは書き換えと、書き換えたコードの間に、コードキャッシュをフラッシュするためにジャンプが必要である。

プロテクトモードで動作するオペレーティングシステム内で書き換えの許可を得るには、特別なシステムコールが必要である。16ビットWindowsではChangeSelector、32ビットWindowsではVirtualProtectとFlushInstructionCacheを呼ぶか、コードをデータセグメントに入れなければならない。

自己改変コードは良いプログラミング習慣とは考えられない。しかし、かなりの速度が稼げるならば、理由を正当化できるかもしれない。

27.10 プロセッサの見分け方 (すべてのプロセッサ)

今まで、あるマイクロプロセッサにとって最適なコードが他のプロセッサでは最適でない可能性はかなり明らかになってきている。プログラムの最も重要な部分を異なるバージョンで作成し、各々のプロセッサ用に最適化しておき、プログラムが走っているプロセッサを検出し、実行時に選択してもよい。すべてのプロセッサでサポートされていない命令を使うならば(即ち条件移動、FCOMI, MMXとXMM命令)、まず最初にプログラムが走っているプロセッサがこれらの命令をサポートしているかどうか調べなければならない。以下のサブルーチンは、マイクロプロセッサの種類と、サポートされている特徴を調べる。

; CPUID命令がわからないアセンブラのためのマクロ定義
CPUID   MACRO
        DB      0FH, 0A2H
ENDM

; C++ prototype:
; extern "C" long int DetectProcessor (void);

; return value: 
; bits 8-11 = ファミリー(PPlainとPMMXは5, PPro, PIIとPIIIは6)
; bit  0 = 浮動小数点命令サポート
; bit 15 = 条件移動命令とFCOMI命令サポート
; bit 23 = MMX命令サポート
; bit 25 = XMM命令サポート

_DetectProcessor PROC NEAR
PUBLIC  _DetectProcessor
        PUSH    EBX
        PUSH    ESI
        PUSH    EDI
        PUSH    EBP
        ; マイクロプロセッサがCPUID命令をサポートしているか調べる
        PUSHFD
        POP     EAX
        MOV     EBX, EAX
        XOR     EAX, 1 SHL 21    ; CPUIDのビットが変えられるか調べる
        PUSH    EAX
        POPFD
        PUSHFD
        POP     EAX
        XOR     EAX, EBX
        AND     EAX, 1 SHL 21
        JZ      SHORT DPEND      ; CPUID命令はサポートされていない
        XOR     EAX, EAX
        CPUID                    ; CPUID機能の結果を得る
        TEST    EAX, EAX
        JZ      SHORT DPEND      ; CPUID機能1はサポートされていない
        MOV     EAX, 1
        CPUID                    ; ファミリーと特徴を得る
        AND     EAX, 000000F00H  ; ファミリー
        AND     EDX, 0FFFFF0FFH  ; 特徴フラグ
        OR      EAX, EDX         ; ビットを結合
DPEND:  POP     EBP
        POP     EDI
        POP     ESI
        POP     EBX
        RET
_DetectProcessor ENDP

いくつかのオペレーティングシステムはXMM命令を許可していないことに気をつけなさい。オペレーティングシステムがXMM命令をサポートしているかどうか調べる方法についての情報はインテルのアプリケーションノートAP-900 "SSE拡張命令とオペレーティングシステムの見分け方 "で見つけられる。マイクロプロセッサの証明についての更なる情報はインテルのアプリケーションノートAP-485 "インテルプロセッサの証明とCPUID命令" で見つけられる。

条件移動、MMX, XMM命令などを持っていないアセンブラでコードするには、www.agner.org/assem/macros.zipのマクロを使いなさい。


28. 命令のタイミング (PPlain and PMMX)

28.1 整数命令 (PPlain and PMMX)

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

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

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

オペコード             オペランド          クロックサイクル    ペア可能性
-------------------------------------------------------------------------
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クロック余計にかかる(
12章参照)。
b) FSとGSのときは0FHプリフィックスを持つ。注a参照。
c) SS,FS,GSのときは0FHプリフィックスを持つ。注a参照。
d) 2オペランドで即値なしのときは0FHプリフィックスを持つ。注a参照。
e) 22章参照。
f) レジスタがアキュームレータのときのみペアにできる。26章14参照。
g) CLDのような複数サイクル命令が先行しなければ、リピートプリフィックスのデコードのために1クロック追加(12章参照)。
h) ペアになるとき、アキュームレータに書き込むかのように扱われる。26章2参照。
i) SPが4で割り切れるときには9。10章2参照。
j) PPlainで特権モードあるいはリアルモードは6、非特権モードでは11、仮想86モードではエラー。PMMXではそれぞれ8と13。

28.2 浮動小数点命令のリスト

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

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

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

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

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

オペコード           オペランド クロックサイクル ペア可能性     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 s)      np             2      2
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                         65-100 r)      np             2      2
FSINCOS                           89-112 r)      np             2      2
F2XM1                              53-59 r)      np             2      2
FYL2X                                103 r)      np             2      2
FYL2XP1                              105 r)      np             2      2
FPTAN                            120-143 r)      np            36 o)   0
FPATAN                           112-134 r)      np             2      2

FNOP                                   1         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クロックサイクルは、先行する整数命令とオーバーラップする。
26章7参照。
r) クロック数は代表的なもの。ささいなケースでは速くなり、極端な例では遅くなり得る。
s) FABS, FCHS, FSTのための出力が必要ならば3クロックほど余計にかかる。

28.3 MMX命令 (PMMX)

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

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

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

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


29. 命令タイミングとμ-OPSへの分解 (PPro, PII and PIII)

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

μ-OPS:
各々の実行ポートで命令が生成するμ-OPSの数
p0: ポート0: ALU, など
p1: ポート1: ALU, ジャンプ
p01: ポート0とポート1のうち、先に空になった方へ行ける命令
p2: ポート2: データの読み出し, など
p3: ポート3: データ書き込みのための番地生成
p4: ポート4: データ書き込み

遅延:
依存の連鎖中にある時、命令が生成する遅延。(これは実行ユニット中で費やされる時間とは違う。値は、それが正確に測定できないため、特にメモリオペランドがある状況では不正確である。)数は最小の値である。キャッシュミス、ミスアラインメント、そして例外はクロック数をかなり増大させる。浮動小数点のオペランドはノーマル数を仮定している。デノーマル数、NANと無限大は50〜150クロック増える。但し、XMM移動、シャッフルと論理命令を除く。

スループット:
いくつかの命令の最大のスループットは同じ種類である。例えば、FMUL命令のスループット1/2は、新しいFMUL命令が2クロック毎に発行できることを意味する。

29.1 整数命令 (PPro, PII and PIII)

オペコード           オペランド        μ-OPS             遅延     スループット
                                  p0 p1 p01 p2 p3 p4
-------------------------------------------------------------------------------
NOP                                     1                                    
MOV                  r,r/i              1                                    
MOV                  r,m                    1                                
MOV                  m,r/i                     1  1                          
MOV                  r,sr               1                                    
MOV                  m,sr               1      1  1                          
MOV                  sr,r         8  -  -                 5                  
MOV                  sr,m         7  -  -   1             8                  
MOVSX MOVZX          r,r                1                                    
MOVSX MOVZX          r,m                       1                             
CMOVcc               r,r          1     1                                    
CMOVcc               r,m          1     1   1                                
XCHG                 r,r                3                                    
XCHG                 r,m                4   1   1   1     high b)            
XLAT                                    1   1                                
PUSH                 r/i                1       1   1                        
POP                  r                  1   1                                
POP (E)SP                               2   1                                
PUSH                 m                  1   1   1   1                        
POP                  m                  5   1   1   1                        
PUSH                 sr                 2       1   1                        
POP                  sr                 8   1                                
PUSHF(D)                          3     11      1   1                        
POPF(D)                           10    6   1                                
PUSHA(D)                                2       8   8                        
POPA(D)                                 2   8                                
LAHF SAHF                               1                                    
LEA r,m                           1                       1 c)               
LDS LES LFS LGS LSS  m                  8   3                                
ADD SUB AND OR XOR   r,r/i              1                                    
ADD SUB AND OR XOR   r,m                1   1                                
ADD SUB AND OR XOR   m,r/i              1   1   1   1                        
ADC SBB              r,r/i              2                                    
ADC SBB              r,m                2   1                                
ADC SBB              m,r/i              3   1   1   1                        
CMP TEST             r,r/i              1                                    
CMP TEST             m,r/i              1   1                                
INC DEC NEG NOT      r                  1                                    
INC DEC NEG NOT      m                  1   1   1   1                        
AAS DAA DAS                          1                                       
AAD                               1     2                 4                  
AAM                               1  1  2                 15                 
MUL IMUL             r,(r),(i)    1                       4        1/1       
MUL IMUL             (r),m        1         1             4        1/1       
DIV IDIV             r8           2     1                 19       1/12      
DIV IDIV             r16          3     1                 23       1/21      
DIV IDIV             r32          3     1                 39       1/37      
DIV IDIV             m8           2     1   1             19       1/12      
DIV IDIV             m16          2     1   1             23       1/21      
DIV IDIV             m32          2     1   1             39       1/37      
CBW CWDE                                1                                    
CWD CDQ                           1                                          
SHR SHL SAR ROR ROL  r,i/CL       1                                          
SHR SHL SAR ROR ROL  m,i/CL       1         1   1   1                        
RCR RCL              r,1          1     1                                    
RCR RCL              r8,i/CL      4     4                                    
RCR RCL              r16/32,i/CL  3     3                                    
RCR RCL              m,1          1     2   1   1   1                        
RCR RCL              m8,i/CL      4     3   1   1   1                        
RCR RCL              m16/32,i/CL  4     2   1   1   1                        
SHLD SHRD            r,r,i/CL     2                                          
SHLD SHRD            m,r,i/CL     2     1   1   1   1                        
BT                   r,r/i              1                                    
BT                   m,r/i        1     6   1                                
BTR BTS BTC          r,r/i              1                                    
BTR BTS BTC          m,r/i        1     6   1   1   1                        
BSF BSR              r,r             1  1                                    
BSF BSR              r,m             1  1   1                                
SETcc                r                  1                                    
SETcc                m                  1       1   1                        
JMP                  short/near      1                             1/2       
JMP                  far          21 -  -   1                                
JMP                  r               1                             1/2       
JMP                  m(near)         1      1                      1/2       
JMP                  m(far)       21 -  -   2                                
conditional jump     short/near      1                             1/2       
CALL                 near            1  1       1   1              1/2       
CALL                 far          28 -  -   1   2   2                        
CALL                 r               1  2       1   1              1/2       
CALL                 m(near)         1  4   1   1   1              1/2       
CALL                 m (far)      28 -  -   2   2   2                        
RETN                                 1  2   1                      1/2       
RETN                 i               1  3   1                      1/2       
RETF                              23 -  -   3                                
RETF                 i            23 -  -   3                                
J(E)CXZ              short           1  1                                    
LOOP                 short        2  1  8                                    
LOOP(N)E             short        2  1  8                                    
ENTER                i,0                12      1   1                        
ENTER                a,b          ca. 18+4b     b-1 2b                       
LEAVE                                   2   1                                
BOUND                r,m          7     6   2                                
CLC STC CMC                             1                                    
CLD STD                                 4                                    
CLI                               9  -  -                                    
STI                               17 -  -                                    
INTO                                    5                                    
LODS                                        2                                
REP LODS                                10+6n                                
STOS                                        1   1   1                        
REP STOS                                ca. 5n a)   -                        
MOVS                                    1   3   1   1                        
REP MOVS                                ca. 6n a)   -                        
SCAS                                    1   2                                
REP(N)E SCAS                            12+7n                                
CMPS                                    4   2                                
REP(N)E CMPS                            12+9n                                
BSWAP                             1     1                                    
CPUID                             23-48                                      
RDTSC                             31 -  -                                    
IN                                18 -  -                 >300               
OUT                               18 -  -                 >300               
PREFETCHNTA  d)      m                      1                                
PREFETCHT0  d)       m                      1                                
PREFETCHT1  d)       m                      1                                
PREFETCHT2  d)       m                      1                                
SFENCE  d)                                      1   1              1/6       
-------------------------------------------------------------------------------
注:
a) ある状況の下では速くなる。詳しくは
26章3を参照。
b) 26章1を参照。
c) ベースレジスタもインデックスレジスタも使わない定数オフセットの時は3。
d) PIIIのみ。

29.2 浮動小数点命令 (PPro, PII and PIII)

オペコード           オペランド        μ-OPS             遅延     スループット
                                  p0 p1 p01 p2 p3 p4
-------------------------------------------------------------------------------
FLD                  r            1                                           
FLD                  m32/64                 1             1                   
FLD                  m80          2         2                                 
FBLD                 m80          38        2                                 
FST(P)               r            1                                           
FST(P)               m32/m64                   1  1       1                   
FSTP                 m80          2            2  2                           
FBSTP                m80          165          2  2                           
FXCH                 r                                    0        3/1 f)     
FILD                 m            3         1             5                   
FIST(P)              m            2            1  1       5                   
FLDZ                              1                                           
FLD1 FLDPI FLDL2E etc.            2                                           
FCMOVcc              r            2                       2                   
FNSTSW               AX           3                       7                   
FNSTSW               m16          1            1  1                           
FLDCW                m16          1     1   1             10                  
FNSTCW               m16          1            1  1                           
FADD(P) FSUB(R)(P)   r            1                       3        1/1        
FADD(P) FSUB(R)(P)   m            1         1             3-4      1/1        
FMUL(P)              r            1                       5        1/2 g)     
FMUL(P)              m            1         1             5-6      1/2 g)     
FDIV(R)(P)           r            1                       38 h)    1/37       
FDIV(R)(P)           m            1         1             38 h)    1/37       
FABS                              1                                           
FCHS                              3                       2                   
FCOM(P) FUCOM        r            1                       1                   
FCOM(P) FUCOM        m            1         1             1                   
FCOMPP FUCOMPP                    1     1                 1                   
FCOMI(P) FUCOMI(P)   r            1                       1                   
FCOMI(P) FUCOMI(P)   m            1         1             1                   
FIADD FISUB(R)       m            6         1                                 
FIMUL                m            6         1                                 
FIDIV(R)             m            6         1                                 
FICOM(P)             m            6         1                                 
FTST                              1                       1                   
FXAM                              1                       2                   
FPREM                             23                                          
FPREM1                            33                                          
FRNDINT                           30                                          
FSCALE                            56                                          
FXTRACT                           15                                          
FSQRT                             1                       69       e,i)       
FSIN FCOS                         17-97 -                 27-103   e)         
FSINCOS                           18-110                  29-130   e)         
F2XM1                             17-48 -                 66       e)         
FYL2X                             36-54 -                 103      e)         
FYL2XP1                           31-53 -                 98-107   e)         
FPTAN                             21-102                  13-143   e)         
FPATAN                            25-86 -                 44-143   e)         
FNOP                              1                                           
FINCSTP FDECSTP                   1                                           
FFREE                r            1                                           
FFREEP               r            2                                           
FNCLEX                                  3                                     
FNINIT                            13 -  -                                     
FNSAVE                            141   -                                     
FRSTOR                            72 -  -                                     
WAIT                                    2                                     
-------------------------------------------------------------------------------
注:
e) パイプライン化されていない。
f) FXCHは一つのμ-OPSを生成し、それはどのポートにも行かずにレジスタ・リネーミングによって解決される。
g) FMULは整数乗算と同じ回路を使用する。それゆえ、浮動小数点と整数の乗算が混合している時、合わせたスループットは3クロックサイクル毎に1 FMUL + 1 IMULである。
h) FDIVの遅延はコントロールワード内で指定された精度に依存する。64ビット精度の遅延は38、53ビット精度の遅延は32、24ビットの遅延は18である。2のべき乗数による除算は2クロックかかる。スループットは 1/(遅延-1)である。
i) 低い精度ではより速い。

29.3 MMX命令 (PII and PIII)

オペコード           オペランド        μ-OPS             遅延     スループット
                                  p0 p1 p01 p2 p3 p4
-------------------------------------------------------------------------------
MOVD MOVQ            r,r                1                          2/1        
MOVD MOVQ            r64,m32/64             1                      1/1        
MOVD MOVQ            m32/64,r64                1  1                1/1        
PADD PSUB PCMP       r64,r64            1                          1/1        
PADD PSUB PCMP       r64,m64            1   1                      1/1        
PMUL PMADD           r64,r64      1                       3        1/1        
PMUL PMADD           r64,m64      1         1             3        1/1        
PAND PANDN POR,                                                               
PXOR                 r64,r64            1                          2/1        
PAND PANDN POR,                                                               
PXOR                 r64,m64            1   1                      1/1        
PSRA PSRL PSLL       r64,r64/i       1                             1/1        
PSRA PSRL PSLL       r64,m64         1      1                      1/1        
PACK PUNPCK          r64,r64         1                             1/1        
PACK PUNPCK          r64,m64         1      1                      1/1        
EMMS                              11 -  -                 6 k)                
MASKMOVQ  d)         r64,r64            1      1  1       2-8      1/30-1/2   
PMOVMSKB  d)         r32,r64         1                    1        1/1        
MOVNTQ  d)           m64,r64                   1  1                1/30-1/1   
PSHUFW  d)           r64,r64,i       1                    1        1/1        
PSHUFW  d)           r64,m64,i       1      1             2        1/1        
PEXTRW  d)           r32,r64,i       1  1                 2        1/1        
PISRW  d)            r64,r32,i       1                    1        1/1        
PISRW  d)            r64,m16,i       1      1             2        1/1        
PAVGB PAVGW  d)      r64,r64            1                 1        2/1        
PAVGB PAVGW  d)      r64,m64            1   1             2        1/1        
PMINUB PMAXUB,
PMINSW PMAXSW  d)    r64,r64            1                 1        2/1        
PMINUB PMAXUB,
PMINSW PMAXSW  d)    r64,m64            1   1             2        1/1        
PMULHUW  d)          r64,r64      1                       3        1/1        
PMULHUW  d)          r64,m64      1         1             4        1/1        
PSADBW  d)           r64,r64      2     1                 5        1/2        
PSADBW  d)           r64,m64      2     1   1             6        1/2        
-------------------------------------------------------------------------------
注:
d) PIIIのみ。
k) EMMSとその後の浮動小数点命令の間に他の命令を挟むことによって遅延を隠すことができる。

29.4 XMM命令 (PIII)

オペコード           オペランド        μ-OPS             遅延     スループット
                                  p0 p1 p01 p2 p3 p4
-------------------------------------------------------------------------------
MOVAPS               r128,r128          2                 1        1/1        
MOVAPS               r128,m128              2             2        1/2        
MOVAPS               m128,r128                 2  2       3        1/2        
MOVUPS               r128,m128              4             2        1/4        
MOVUPS               m128,r128       1         4  4       3        1/4        
MOVSS                r128,r128          1                 1        1/1        
MOVSS                r128,m32           1   1             1        1/1        
MOVSS                m32,r128                  1  1       1        1/1        
MOVHPS MOVLPS        r128,m64           1                 1        1/1        
MOVHPS MOVLPS        m64,r128                  1  1       1        1/1        
MOVLHPS MOVHLPS      r128,r128          1                 1        1/1        
MOVMSKPS             r32,r128     1                       1        1/1        
MOVNTPS              m128,r128                 2  2                1/15-1/2   
CVTPI2PS             r128,r64        2                    3        1/1        
CVTPI2PS             r128,m64        2      1             4        1/2        
CVTPS2PI CVTTPS2PI   r64,r128        2                    3        1/1        
CVTPS2PI             r64,m128        1      2             4        1/1        
CVTSI2SS             r128,r32        2      1             4        1/2        
CVTSI2SS             r128,m32        2      2             5        1/2        
CVTSS2SI CVTTSS2SI   r32,r128        1      1             3        1/1        
CVTSS2SI             r32,m128        1      2             4        1/2        
ADDPS SUBPS          r128,r128       2                    3        1/2        
ADDPS SUBPS          r128,m128       2      2             3        1/2        
ADDSS SUBSS          r128,r128       1                    3        1/1        
ADDSS SUBSS          r128,m32        1      1             3        1/1        
MULPS                r128,r128    2                       4        1/2        
MULPS                r128,m128    2         2             4        1/2        
MULSS                r128,r128    1                       4        1/1        
MULSS                r128,m32     1         1             4        1/1        
DIVPS                r128,r128    2                       48       1/34       
DIVPS                r128,m128    2         2             48       1/34       
DIVSS                r128,r128    1                       18       1/17       
DIVSS                r128,m32     1         1             18       1/17       
ANDPS ANDNPS,
ORPS XORPS           r128,r128       2                    2        1/2        
ANDPS ANDNPS,
ORPS XORPS           r128,m128       2      2             2        1/2        
MAXPS MINPS          r128,r128       2                    3        1/2        
MAXPS MINPS          r128,m128       2      2             3        1/2        
MAXSS MINSS          r128,r128       1                    3        1/1        
MAXSS MINSS          r128,m32        1      1             3        1/1        
CMPccPS              r128,r128       2                    3        1/2        
CMPccPS              r128,m128       2      2             3        1/2        
CMPccSS              r128,r128       1      1             3        1/1        
CMPccSS              r128,m32        1      1             3        1/1        
COMISS UCOMISS       r128,r128       1                    1        1/1        
COMISS UCOMISS       r128,m32        1      1             1        1/1        
SQRTPS               r128,r128    2                       56       1/56       
SQRTPS               r128,m128    2         2             57       1/56       
SQRTSS               r128,r128    2                       30       1/28       
SQRTSS               r128,m32     2         1             31       1/28       
RSQRTPS              r128,r128    2                       2        1/2        
RSQRTPS              r128,m128    2         2             3        1/2        
RSQRTSS              r128,r128    1                       1        1/1        
RSQRTSS              r128,m32     1         1             2        1/1        
RCPPS                r128,r128    2                       2        1/2        
RCPPS                r128,m128    2         2             3        1/2        
RCPSS                r128,r128    1                       1        1/1        
RCPSS                r128,m32     1         1             2        1/1        
SHUFPS               r128,r128,i     2  1                 2        1/2        
SHUFPS               r128,m128,i     2      2             2        1/2        
UNPCKHPS UNPCKLPS    r128,r128       2  2                 3        1/2        
UNPCKHPS UNPCKLPS    r128,m128    2  2                    3        1/2        
LDMXCSR              m32          11 -  -                 15       1/15       
STMXCSR              m32          6  -  -                 7        1/9        
FXSAVE               m4096        116   -                 62                  
FXRSTOR              m4096        89 -  -                 68                  
-------------------------------------------------------------------------------


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

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

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

;************   Test program for PPlain and PMMX:    ********************
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とPIIIでは、RDTSCが他と並列に実行されないように、CPUIDのようなシリアル化する命令を各RDTSCの前後に置かなければならない。(CPUIDはシリアル化命令である。つまり、パイプラインをフラッシュして、進む前にペンディングしている操作がすべて完了するのを待つ。これはテストの目的には便利である。CPUIDは引き続く命令のプリフィックスをデコードできる影を持たない。)

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

完全な形のテストプログラムはwww.agner.org/assem/から利用可能である。

Pentiumプロセッサは特別な性能モニタ用のカウンタを持っており、キャッシュミス、ミスアラインメント、各種のストールなどのようなできごとを数えることができる。性能モニタ用カウンタの使い方についてはこのマニュアルではカバーしないが、"インテル・アーキテクチャ・ソフトウェア・デベロッパーズ・マニュアル"第3巻, 付録Aに書かれている。


31. いろいろなマイクロプロセッサの比較

下の表はペンティアムファミリーのマイクロプロセッサのいくつかの重要な違いについて要約している。

                               PPlain PMMX   PPro   PII    PIII 
----------------------------------------------------------------------
コードキャッシュ(KB)           8      16     8      16     16
データキャッシュ(KB)           8      16     8      16     16
内蔵L2キャッシュ(KB)           0      0      256    512*)  512*)
MMX命令                        なし   あり   なし   あり   あり
XMM命令                        なし   なし   なし   なし   あり
条件移動命令                   なし   なし   あり   あり   あり
アウト・オブ・オーダー実行     なし   なし   あり   あり   あり
分岐予測                       劣る   優れる 優れる 優れる 優れる
BTBのサイズ                    256    256    512    512    512
RSBのサイズ                    0      4      16     16     16
分岐予測ミスのペナルティ       3〜4   4〜5   10〜20 10〜20 10〜20
パーシャル・レジスタ・ストール 0      0      5      5      5
FMULの遅延                     3      3      5      5      5
FMULのスループット             1/2    1/2    1/2    1/2    1/2
IMULの遅延                     9      9      4      4      4
IMULのスループット             1/9    1/9    1/1    1/1    1/1
----------------------------------------------------------------------
*) Celeron: 0〜128KB、Xeon: 512以上、他に異なるものがたくさん入手できる。いくつかのバージョンではL2キャッシュは(コアの)半分の速度で動作する。

訳注: 翻訳時点では更にXeon(Coppermine)、PentiumIII(Coppermine)、Celeron(Coppermine)が存在する。CoppermineコアはXMM命令を持っている。

表の注釈
コードキャッシュの大きさはプログラムの重要な部分が小さなメモリ領域に限定されていない時に重要である。

データキャッシュの大きさは重要な部分でより大きなデータを扱うすべてのプログラムで重要である。

MMX命令とXMM命令は多量のデータを並列に扱う時、つまり音と画像の処理のプログラムに便利である。他のアプリケーションではMMX命令とXMM命令の利点を発揮することはできないかもしれない。

条件移動命令は条件分岐の予測の低下を避けるために便利である。

アウト・オブ・オーダー実行は実行速度、特に最適化されていないコードの速度を改善する。これは自動的な命令の並べ替えとレジスタ・リネーミングを含む。

条件分岐予測法が優れているプロセッサは簡単な繰り返しパターンを予測できる。優れた分岐予測は分岐予測ミスのペナルティが高価な時に最も重要である。

リターン・スタック・バッファ(RSB)はサブルーチンが交互に異なる場所から呼び出される時の予測を改善する。

パーシャル・レジスタ・ストールは混合したサイズ(8, 16, 32ビット)のデータの扱いを更に難しくする。

乗算命令の遅延は依存の連鎖がある時の処理時間である。1/2のスループットは、新規の乗算が2クロックサイクル毎に実行できるよう、実行ユニットがパイプライン化されていることを意味している。これは並列なデータを扱う速度を定める。

このドキュメントに書かれた大部分の最適化手法は他のプロセッサ(インテルではないプロセッサを含む)では大した害を与えない、または全く与えない。しかしながら何らかの問題があることに気がついていなさい。

PPlainとPMMXのための浮動小数点コードのスケジューリングはしばしば多くのFXCH命令を必要とする。これは古いマイクロプロセッサでは速度を低下させるが、ペンティアムファミリーと、進歩したインテルではないプロセッサではそういうことはない。

PMMX、PIIとPIIIプロセッサにおいてMMX命令の優位性を引き出すと、またPPro, PII, PIIIプロセッサにおいて条件移動命令を利用すると、初期のマイクロプロセッサとのコンパチビリティにおいて問題を起こす。解決にはいくつかのバージョンのコードを書いておき、実行時に適切なバージョンのコードを選択すればよい(27章10を見よ)。