アセンブラでの高速化

  1. はじめに
  2. 共通/80286以前
  3. 80386以降
  4. 80486以降
  5. Pentium
  6. PentiumPro以降
  7. Pentium4
  8. 高速化の例
  9. 主な命令一覧
  10. 参考文献

はじめに

アセンブリ言語でプログラムを書くということは、プログラムに最高の自由度と性能を与えるということである。アセンブリ言語ではコンピュータの持つ機能をすべて使えるので、高級言語では不可能だった処理が可能になるし、高級言語からは見えないCPUの機能を利用してプログラムを高速にしたり、プログラムを小さくしたりすることができる。特にインテルの8086とその後継プロセッサ(86系プロセッサ)の場合、アセンブリ言語で書き直すだけでプログラムが大幅に小さく、そして速くなることは多い。しかし、ただ単純にアセンブリ言語で書いただけでは、コンピュータはその本来の力を発揮しているとは言い難い。コンピュータには、もっと速く実行する可能性が秘められているのである。

この文章では、アセンブリ言語で書かれたプログラムをさらに高速化し、CPUの持つ性能を十分に発揮させる方法を説明する。高級言語で書かれたプログラムの高速化に関して言われているような点について重複して述べることはしない。つまり、

などについては特に解説しない。

86系CPUには、8086、8088、80186、80286、80386、80486、Pentium、PentiumProなど、および各種の互換プロセッサがある。この文章では、8086、80286、80386、80486、Pentium、PentiumProを扱っている。次のように分けて解説しているので、CPUに応じて、必要な章を読んでほしい。

例えば、PentiumProの場合には、共通、80386以降、80486以降、PentiumProの章を読んでほしい。なお、8088はデータバス幅が半分の8086、80386SXはデータバス幅が半分の80386とみなせるので、メモリアクセスに追加クロックが必要(8088は4クロック以上、80386SXは2クロック以上、メモリアクセスのウェイト数による)な他は、8086,80386と傾向は同じである。

この文章では、レジスタを次のように表記する。文中であっても「AXレジスタ」などとは書かずに単に「AX」とだけ書くことにする。

8ビットレジスタ
AL,CL,DL,BL,AH,CH,DH,BH
16ビットレジスタ
AX,CX,DX,BX,SP,BP,SI,DI
32ビットレジスタ
EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI
セグメントレジスタ
ES,CS,SS,DS,FS,GS
命令ポインタ(16,32ビット)
IP,EIP
フラグレジスタ(16,32ビット)
FLAGS,EFLAGS
個々のフラグ
CF,PF,AF,ZF,SF,TF,IF,DF,OF

共通/80286以前

この章では、86系CPUに共通する最適化と、80286以前のCPUに特有の最適化について述べる。80386以降では逆効果になるものについては、それぞれ注意書きをつけてある。

最適化の基本は、実行される命令数を少なくしたり、実行時間(クロック数)の短い命令を使ったりして、プログラムの実行に必要な合計のクロック数を減らすことである。実行される命令数を少なくすることはたいてい、プログラム中の命令数を少なくすることになるし、実行時間の短い命令を使うことはたいてい、短い(バイト数の少ない)命令を使うことになるので、実行時間の最適化は、同時にプログラムのサイズの最適化になることが多い。

例えば、BXが0かどうか調べる場合、普通に

	CMP BX,0
とすると、3バイト必要だが、
	TEST BX,BX
とすれば、2バイトですむ。また、実行に必要なクロック数は、次のように、短縮されるか同じになる(Bytesの下の数字は命令のバイト数を、8086などの下の数字は、各プロセッサで実行に必要な最小のクロック数を表す)。
		Bytes	8086	80286	80386	80486
CMP BX,0	3	4	3	2	1
TEST BX,BX	2	3	2	2	1
この二つの命令は、AFを除いて同じ動作をするので、たいていの場合は前者を後者で置き換えると改良になる。TEST命令の代わりに、AND命令またはOR命令を用いても同じである。

命令自体の実行時間が同じでも、あるいはむしろ長くなっても、プログラムを短くすることは有利な場合が多い。短いプログラムは、ディスクなどからメモリに読み込む時間が短くてすみ、CPUが命令をメモリからフェッチする時間が短くなり、キャッシュの使用効率(80486以降)が上がる。だから、繰り返し何度も実行されるような、速度的に重要な部分だけは、実行に必要なクロック数に注目して最適化し、それ以外は、主にコードの長さ(バイト数)に注目して最適化するのがよい。

0に関する操作

定数0に関係する操作は、特別な命令(列)を使うことで、一般の定数の場合より短いコードにできることが多い(もちろん、0の加算などはすっかり省略してよい)。これには、次のようなものがある。

なお、メモリ上のデータが0かどうか調べるには、素直に

	CMP [ADDR],0
とするのがよい。頻繁に調べるなら、レジスタに0を入れておいて使うとよい。

INC,DEC命令

INC,DEC命令は、オペランドに1を加減算する命令で、ADD,SUB命令の特別な場合(1または2バイト短い)として使えるが、INC,DEC命令ではCFが変化しないことを利用すると、応用範囲が広がる(PentiumProではストールに注意)。例えば、多倍長の加算をするときにCFを別に保存する必要がない。
例
;SIが指す多倍長数を、DIが指す多倍長数に加える
;多倍長数の長さはCXワード
	CLC
L1:	LODSW
	ADC [DI],AX
	INC DI
	INC DI
	LOOP L1
	JC L2 ; オーバーフローしたときの処理
逆に、CFを変化させる必要があるときは、ADD,SUB命令を使う。

ワードレジスタに対するINC,DEC命令は1バイトですむので、コードを短くするのに便利である。ワードレジスタに対する2までの加減算はINC,DEC命令を使うとよい。ただし、少し遅くなる。

		Bytes	8086	80286	80386	80486
ADD BX,2	3	4	3	2	1
INC BX / INC BX	2	6	4	4	2

バイトレジスタに対してINC,DEC命令を使いたいときも、ワードレジスタに対する命令を使ったほうがよいこともある。例えばBXの下位バイトが0FFHでないことがわかっている場合、INC BLの代わりにINC BXを使うと1バイト短くなる。

オペランドの値が0であることがわかっているとき、INC命令で1にしたり、DEC命令で0FFFFhにしたりすることができる。

		Bytes	8086	80286	80386	80486
MOV AX,1	3	4	2	2	1
INC AX		1	3	2	2	1
これは、レジスタを真偽値として使うときに便利である。

オペランドの値が変化してよいときには、INC命令で0FFFFhかどうか調べたり、DEC命令で1かどうか調べたりすることができる。

		Bytes	8086	80286	80386	80486
CMP AX,1	3	4	3	2	1
DEC AX		1	3	2	2	1
続けて使うと0FFFEh,0FFFDh,0FFFCh,…、あるいは2,3,4,…かどうか調べることもできる。

アキュームレータ

アキュームレータ(AL,AX,EAX)に関する命令には、他のレジスタを使う場合に比べて1バイト短いエンコーディングを持つものがある。これを使っても命令自体のクロック数はたいてい変わらないが、コードを短くすることができる。これには、次のものがある。
AL,AX,EAXと即値のADD,ADC,SUB,SBB,AND,OR,XOR,CMP,TEST命令(8086ではTEST命令で1クロック短縮)
AL,AX,EAXからメモリ(レジスタ間接は使えない)へのMOV命令(8086では5クロック短縮)
メモリ(レジスタ間接は使えない)からAL,AX,EAXへのMOV命令(8086では4クロック短縮)
AX,EAXとレジスタのXCHG命令(Pentiumでは1クロック短縮)
だから、このような演算をよくするデータは、なるべくアキュームレータに入れておくのがよい。

即値

即値とのADD,ADC,SUB,SBB,AND,OR,XOR,CMP命令は、即値の値が-128〜+127のときは、範囲外のときに比べて1バイト短いエンコーディングを持つ。これを使っても命令自体のクロック数は変わらないが、コードを短くすることができる。なお、上で述べた、アキュームレータに関する短縮形には、このエンコーディングはない。即値とのTEST命令にもこのエンコーディングはないが、部分レジスタを使うことで同じ効果を得られる。

レジスタの上位バイトまたは下位バイトだけに関する、即値との論理演算は、部分レジスタを使うと1バイト短くなる。

例
AND BX,0FFFEH → AND BL,0FEH (フラグは異なる)
OR BX,8000H → OR BH,80H (フラグは異なる)
TEST BX,0006H → TEST BL,06H
特別な擬似命令で、この最適化を自動的に行うアセンブラもある(TASM3.0のMASKFLAG,SETFLAG,FLIPFLAG,TESTFLAGなど)。80486とPentiumProでは部分レジスタストールに注意する。

逆に、レジスタ全体についての命令を使って、部分レジスタを二つ同時に変更することができる。例えば、桁上がりがなければ、 ADD AX,0FF01H は、ALに1を加え、AHから1を減ずる。

アドレシングモード

80286以前では、メモリを参照するアドレシングモードは、次の16通り(変位の大きさを入れると24通り)である。
[BX+SI]		[BX+SI+disp]
[BX+DI]		[BX+DI+disp]
[BP+SI]		[BP+SI+disp]
[BP+DI]		[BP+DI+disp]
[SI]		[SI+disp]
[DI]		[DI+disp]
[offset]	[BP+disp]
[BX]		[BX+disp]
offsetは16ビットのオフセットアドレス
dispは8または16ビットの変位(定数)
アドレスの計算の際、限定されてはいるが、レジスタ同士や、レジスタと変位、レジスタ同士と変位の加算ができるので、これを利用して命令数を減らすことができる。例えば次のようなときである。 不必要に複雑なアドレシングモードを使うと遅くなるので注意する(命令一覧の「16ビットアドレシングの追加バイト数と追加クロック数」を参照)。

アドレス指定で変位を使う場合、変位が8ビットで収まる(-128〜+127)ときには、範囲外のときに比べて1バイト短くなる。変位が0のときは変位なしとみなされて、2バイト短くなり、場合によっては実行時間も短縮される。

ただし、BP間接だけは、変位なしのエンコーディングがないので、変位が128〜+127のときに1バイト短くなるだけである。この他、BPを含むアドレシングモード([BP+SI]なども含む)は、デフォルトのセグメントがSSである点も特殊なので、注意して使うべきである。

LEA命令

LEA命令は、オペランドのメモリをアクセスするのではなく、そのオフセットアドレスをレジスタに入れる命令である。この命令の利点は、次の例のように、アドレス計算の結果を任意のレジスタに入れられることである。

単純に、あるアセンブラシンボルのオフセットアドレスをレジスタに入れるなら、LEA命令ではなくMOV命令を使うべきである。

ローテート/シフト

8086では、1以外の定数によるローテート/シフト命令を持たないため、CLで回数を指定する形式を使いたくなるが、ローテート/シフト命令を繰り返したほうがよいことが多い。 80186以降では、定数の値によって、 ROL AX,n と、n回の ROL AX,1 を使い分けるとよい。なお、8回ローテート/シフトするには、部分レジスタを使うとよい。 80386以降ではかえって遅くなることがあるので注意する。

ローテート/シフト命令と加算命令を置き換えると、命令の長さが同じで時間が短くなることがある。8086と80386以降では実行時間が逆転するので注意する。

オペランドの値が0または1であることがわかっているとき、SHR命令を使うと、オペランドを0にすると同時に、元のオペランドをCFにコピーすることができる。真偽値のクリアとテストを同時に行うのに便利である。真偽値をクリアでなくセットしたければ、続けてINC命令を使う(INC命令はCFを変更しない)。

80186以降では、オペランドの符号を値として設定したいときに、SHR,SAR命令を使うとよい。

負なら1	SHR AX,15
負なら-1	SAR AX,15

ジャンプ

ジャンプ命令は、実行時間のかかる命令なので、なるべく使わないようにする。条件ジャンプを使う場合には、なるべくジャンプしなくてすむようにする。例えば、次のコード

	TEST BP,BP
	JNZ L1
	MOV AX,100
	JMP L2
L1:	MOV AX,200
L2:
で、BPが0でないことが多いなら、次のように置き換えるとよい。
	MOV AX,100
	TEST BP,BP
	JZ L2
	MOV AX,200
L2:
Pentium以降では、分岐予測が成功すれば、ジャンプ命令の実行時間は問題ではなくなった。また、MMXなしPentiumの分岐予測では、条件ジャンプはなるべくジャンプするようにするとよい。

フラグをうまく使うと、条件ジャンプ命令をなくすことができる(フラグの節を参照)。

無条件ジャンプのジャンプ先が1バイトまたは2バイト先で、フラグが変化してよいときは、JMP命令の代わりに、

	DB 0A8H ; TEST AL,n
または
	DB 0A9H ; TEST AX,nn
を使うとよい。こうすると、スキップしたい1バイトまたは2バイトの命令(列)をTEST命令の即値データとして使ってしまうので、フラグの変化以外は何もしないで、次の命令(ジャンプ先)の実行が始まることになる。ただし、Pentium以降では、命令キャッシュに命令の境界を記憶して、次回の実行で利用しているので、このようなコードは避けたほうがよい。

ジャンプ先が同一セグメント内のJMP命令は、ジャンプ先が-128〜+127バイトの範囲なら、短い形のエンコーディング(short形式)を利用できる。だから、ジャンプ先がなるべく近くになるようにルーチンを並べ変えれば、コードを短くすることができる。

マルチパスのアセンブラ(TASMで/Mスイッチをつけたときなど)では、可能なら自動的にshort形式を使ってくれる。そうでないアセンブラを使うときには、オペランドの前に「SHORT」をつける。

80286以前では、条件つきジャンプ命令にはshort形式しかないので、ジャンプ先が-128〜+127バイトの範囲にない場合には、無条件ジャンプ命令と組み合わせる必要がある。マルチパスのアセンブラ(TASMで/Mスイッチをつけ、JUMPS擬似命令を使った場合など)では、必要なら自動的に、例えば

	JAE L3
	JNAE L99
	JMP L3
L99:
にするような処理をしてくれる。だから、アセンブラに頼ってもよいのだが、もしこの例でほとんどの場合にL3にジャンプしないのなら、
	JAE TO_L3
として、近くの別のところに
TO_L3:	JMP L3
を置いたほうがよい。こうすると、同じラベルにとぶ別のジャンプ命令と共有することもできる。速度的に重要なコードの部分では、なるべくジャンプ先がshort形式で届くようにルーチンを配置し、届かないところはジャンプする割合を考えて、上のどちらかの形式に条件ジャンプを書き換えるようにするとよい。

マルチパスでないアセンブラでも、マクロを使えば、上のL99を使う形式を自動的に生成できる(前方参照のラベルが128バイトまでに入った場合でもこうなってしまうが)。このとき生成した JMP L3 の位置もマクロで覚えておけば、以後の同じラベルにとぶジャンプ命令で、TO_L3の代わりに使える。

コール/リターン

	CALL SUB1
	RET
のように、CALL命令とRET命令が続く場合は、
	JMP SUB1
に置き換えることができる。ただし、CALLとRETの種類(near/far)が同じでなければならない。

条件つきRET命令はないので、条件ジャンプ命令と組み合わせて使う。

	JNAE L99
	RET
L99:
のように、条件を反転させてもよいが、たいていはリターンしないのであれば、
	JAE L_RET
として、近くの別のところに
L_RET:	RET
を置くのがよい。もちろん、これは前のサブルーチンの終わりなどのRET命令と共有できる。ただし、MMXなしPentiumでは、RET命令の共有は分岐予測ミスによる速度低下の原因となることがある。

速度的に重要な部分では、サブルーチンの呼び出しとリターンにかかる時間を節約するために、サブルーチン本体を呼び出し場所に埋め込む(インライン展開)とよい。長いサブルーチンなら、サブルーチンの中で頻繁に使われる部分だけを埋め込むこともできる。例えば、

SUB1:	CMP SI,[BUFFER_END]
	JNE L1
	;
	; 長い処理
	;
L1:	LODSB
	RET
のようなサブルーチンなら、CXにSIから[BUFFER_END]までのバイト数を入れることにして、
	LOOP L1
	CALL SUB2
L1:	LODSB
をサブルーチン呼び出し場所に埋め込み、
SUB2:	;
	; 長い処理
	;
	MOV CX,[BUFFER_END]
	SUB CX,SI
	RET
のようなサブルーチンを用意する。

farルーチンは、呼び出しやリターンに時間がかかるので、なるべく使わないようにする。コードを複数のセグメントに分けて書かなければならない場合でも、なるべく同じセグメント内で処理がすむようにする。場合によっては、farコール用のエントリを別に作ったり、ルーチンを二つのセグメントにコピーしたりしたほうがよいこともある。

同じセグメントのfarルーチンを呼ぶときには、次の置き換えが可能である。

レジスタ

レジスタをオペランドにする命令は、メモリをオペランドにする命令と比べて、短く速い(命令一覧参照。メモリアクセスのウェイト数や、80486以降のキャッシュミスによっては、差が広がる)。また、レジスタにしかできない操作も多い。だから、計算に必要な値は、なるべくレジスタに入れておくのがよい。また、即値を使うよりレジスタを使ったほうが短く速いことが多いので(命令一覧参照。ローテート/シフト命令は例外)、同じ即値を頻繁に使うならレジスタに入れて使ったほうがよい。ただし、元々メモリにあるデータに1回だけ演算をするときには、メモリをオペランドにするほうがよい。例えば次のような場合である。

86系CPUの汎用レジスタは、SPを除いて7個しかなく、アドレシングモードや、いくつかの命令では特定のレジスタしか使えないため、レジスタ割り付けは簡単ではない。しかし、よく考えれば多くの場合、速度が重要なコード部分で使っているほとんどすべての値をレジスタに置くことができる。

AX,CX,DX,BXの四つのレジスタは、AL,AH,CL,CH,DL,DH,BL,BHのように二つずつに分けて使うことができる。値を1バイトで表現できるときには、これらのレジスタを使うとよい。1バイトでは表現できないように見えても、実際に必要なのは1バイトだけですむ場合もあるので、注意する。例えば、上位バイトが共通な二つの数を使う場合などである。

1ビットで表現できるような値をたくさん使うときには、レジスタの各ビットをそれに割り当てて、AND,OR,XOR,TEST命令で操作するとよい。

レジスタ毎に性格が異なるため、以下に示すレジスタの特徴を考慮して、値を割り当てるレジスタを決める。

AX
乗除算の演算数や結果。ストリング操作命令のデータ。IN,OUT命令のデータ。他のレジスタに比べて、1バイト短いエンコーディングを持つ命令がある。
CX
ループ命令やストリング操作命令のカウンタ。CLは、ローテート/シフト命令のカウンタ。
DX
16ビット乗算の結果の上位が入る。16ビット除算の被除数の上位を入れる。16ビット除算の結果の剰余が入る。IN,OUT命令のポートアドレス。
BX
バイトレジスタに分割できるレジスタのうち、アドレス指定に使える唯一のもの。
SP
スタックポインタ。普通は他の用途には使わない。
BP
アドレス指定に使えるが、デフォルトセグメントがSSなので注意。
SI
アドレス指定に使える他、ストリング操作命令の転送元アドレスを指定する。
DI
アドレス指定に使える他、ストリング操作命令の転送先アドレスを指定する。

CやPascalなどのコンパイル結果では、スタックフレームにローカル変数を置き、BPを使ってアクセスする方法がよく使われる。しかし、最初からアセンブリ言語で書かれたプログラムでは、普通はスタックフレームを使わないので、BPを、カウンタ、真偽値、一時記憶などに使うことができる。

サブルーチンに引数を渡したり、サブルーチンから結果を受け取ったりするときには、なるべくレジスタを使う。どのレジスタを使うかは、いっしょに使う他のルーチンとの兼ね合いで決める。アセンブリ言語では、サブルーチンをどこから呼び出すかはすべて把握できるので、重要な呼び出し場所で都合がよいように、レジスタの割り当てを決めればよい。

場合によっては、セグメントレジスタを値の一時記憶として使ってもよい。ただし、80286以降のプロテクトモードでは、セグメントレジスタに好きな値を入れることはできない。

スタック

86系CPUは、スタックに値を積んだり降ろしたりするための、専用の命令を持っている。特に、レジスタのPUSH,POP命令は1バイトですむ。レジスタが足りなくなったときには、しばらく使わない値をPUSHしておき、使う前にPOPするとよい。例えば、次のようにする。
	MOV CX,10
L1:	PUSH CX
	;
	; いろいろな処理
	;
	POP CX
	LOOP L1
PUSHするレジスタとPOPするレジスタは異なってもよい。ただし、PUSHする回数とPOPする回数が異なるとスタックポインタがずれてしまうので注意する。途中で条件ジャンプが入るときには、間違えやすいので特に注意する。

PUSH,POP命令は、メモリをオペランドにすることもできる。メモリ上のデータをコピーするときに、空いているレジスタがなければ、PUSHとPOPを使ってスタック経由でコピーするとよい(80486以降では逆効果)。

						Bytes	8086	80286	80386	80486
PUSH AX / MOV AX,[SI] / MOV [DI],AX / POP AX	6	46	16	12	4
PUSH WORD PTR [SI] / POP WORD PTR [SI]		4	43	10	10	10
MOVSW (参考)					1	18	5	8	7

セグメントレジスタを直接コピーする命令はないので、普通は汎用レジスタを経由するが、スタックを経由するとコードが短くなる。

			Bytes	8086	80286	80386	80486
MOV AX,DS / MOV ES,AX	4	4	4	4	6
PUSH DS / POP ES	2	18	8	9	6
セグメントレジスタの値の交換も、スタックを経由すれば、汎用レジスタを使わずにできる。

80186以降では、セグメントレジスタに即値を入れたいときにも、スタックを経由するとコードが短くなる。セグメントアドレスが1バイトで表せるときは、さらに1バイト短くなる。

				Bytes	8086	80286	80386	80486
MOV AX,DGROUP / MOV ES,AX	5	6	4	4	4
PUSH DGROUP / POP ES		4		8	9	4
PUSH 0 / POP ES			3		8	9	4

8086には、定数をPUSHする命令がないので、空いているレジスタに定数を入れてPUSHする。アセンブラによっては、8086用にアセンブルするモードで

	PUSH 999
と書くと、次のような10バイトの命令列
	PUSH AX
	PUSH BP
	MOV BP,SP
	MOV [BP-2],999
	POP BP
を生成するものもある(TASM2.0など)が、遅いのであまり使わないほうがよい。

サブルーチンの入口で、使うレジスタをPUSHし、出口でPOPすると、サブルーチン中でレジスタを、高級言語のローカル変数のように使うことができる。サブルーチンの再帰呼び出しにも対応できる。

スタックポインタ(SP)に対して直接演算してもよい(80486以降ではアドレス生成インターロックに注意)。例えば、 ADD SP,6 とすると、スタックに積んだ値を三つ捨てることができる。

				Bytes	8086	80286	80386	80486
POP AX / POP AX / POP AX	3	24	15	12	3
ADD SP,6			3	4	3	2	1
また、次のようにすれば、高級言語のコンパイラが生成するコードと同じように、まとまったワークエリアをスタック上にとって、BPを使ってアクセスすることができる。
	PUSH BP
	MOV BP,SP
	SUB SP,100
	;
	; 処理
	;
	MOV SP,BP
	POP BP
	RET
この方法も再帰呼び出しに対応できる。ただし、大きなワークエリアをとるときは、スタックがオーバーフローしないように注意する。

スタックを、データの順序を反転するための一時記憶として使うこともできる。例えば、10進数表示ルーチンを、10での除算の繰り返しで実現する場合、桁の順序を反転する必要があるが、スタックを使うと次のように書ける。

;AXを10進で表示する
;AX,CX,DXは破壊される
;PRCHRはALの文字を表示するサブルーチン
PRDEC:	XOR CX,CX
L1:	XOR DX,DX
	PUSH CX
	MOV CL,10 ; CH=0
	DIV CX
	POP CX
	PUSH DX
	INC CX
	TEST AX,AX
	JNZ L1
L2:	POP AX
	ADD AL,'0'
	CALL PRCHR
	LOOP L2
	RET

サブルーチンのリターンアドレスもスタックに積まれるので、スタックを操作する命令と組み合わせて使うことができる。例えば、nearサブルーチンで ADD SP,2 とすると、リターンアドレスを捨てることができるし、 POP BX とすると、リターンアドレスをBXに入れることができる。また、

	CALL SUB1
	JMP SUB2
の代わりに
	PUSH OFFSET SUB2
	JMP SUB1
を使うようなことができる。ただし、MMX対応PentiumおよびPentiumPro以降では、このような書き方はリターンアドレススタックの働きの妨げになるので、避ける。

なお、

	PUSH BX
	RET
とするよりは、 JMP BX のほうがよい。farジャンプのときには、専用の命令がないので、
	PUSH ES
	PUSH BX
	RETF
のようにするとよい。

リロケート可能なルーチンで命令ポインタ(IP)の値を知りたいときには、CALL命令が相対アドレス指定であることを利用して、次のようにする。

	CALL L3
L3:	POP BX
こうすると、L3の実際のオフセットアドレスがBXにはいる。

サブルーチンの最後で別のサブルーチンを呼ぶ場合、

	CALL SUB
	RET
ではなく
	JMP SUB
のほうが短く速い。テイルリカージョンをループに書き換えるのは、これの特別な場合と考えられる。

複数のスタックを切り替えて使うときには、ときどきSSとSPを変更する必要がある。SSを変更する命令の直後(8086ではセグメントレジスタを変更する命令の直後)には、割り込み(NMIも含む)がかからないようになっているので、SS,SPの順に変更すればCLI命令を使う必要がない。

フラグ

フラグレジスタ(FLAGS)のうち、演算に関係するCF,PF,AF,ZF,SF,OFをここで扱う。これらのフラグを有効に利用すると、高級言語から使っていたのでは考えられないような命令列を書くことができる。高級言語風に考えると、フラグの役割は次の通りである。 アセンブリ言語では、この他に、次のような役割もある。

フラグにはこの他にも、いろいろな使い道がある。

1ビットの値の受け渡し
CFやZF、場合によってはPF,SF,OFを値の受け渡しに使う。サブルーチンのエラーや、判定結果などを返すのに便利である。MS-DOSのファンクションリクエストにも、CFやZFで結果を返すものがある。受け側では普通、条件ジャンプ命令で処理を分けることになる。

CFには専用のセット、リセット、反転命令があり、独立して操作できるが、他のフラグはそうではない。そのため、可能な状態を考えて値をフラグに割り当てる必要がある。ZFをセットするには、 CMP AL,AL を使うとよい(CF,SF,OFはリセットされる)。ZFをリセットするには、レジスタが変化するが、 OR AL,0FFh などを使うとよい(CF,OFはリセットされ、SFはセットされる)。CFは、他のフラグをいじるとリセットされてしまうことが多い(AND,OR,XOR,TEST命令では必ずリセットされる)ので、後でSTC命令を使ってセットするのもよい。

フラグを設定するときに、わざわざそのための命令を使う必要は必ずしもない。例えば、リングバッファが空かどうかをZFに返すルーチンは、リングバッファの先頭と末尾のポインタを比較して、そのままリターンすればよい。また、ALに数字のASCIIコードが入っているかどうかをCFに返すルーチンは、次のようにすればよい。

	CMP AL,'0'
	CMC
	JNC NO
	CMP AL,'9'+1
NO:	RET
ALが変化してよければ、
	SUB AL,'0'
	CMP AL,10
	RET
でよい。このように、実際の処理で使いやすいようにフラグに値を割り当てるとよい。

もし、値が0かどうかによって、CFを設定しなければならなくなったら、次のようにする。

AXが0ならCFをセット	CMP AX,1
AXが0以外ならCFをセット	NEG AX
逆に、CFの値によってレジスタに0または0FFFFhを入れ、ZFをその通りに設定するには、 SBB AX,AX などを使う(CFは保存される)。

条件ジャンプ命令には、JBE命令(CFとZFのどちらかがセットされていたらジャンプ)のように、フラグを複合して調べる命令もある。これを使うと便利な場合もある。

CFの値を使った演算
例えば、AXが100未満ならCXに1を加えたいなら、
	CMP AX,100
	ADC CX,0
とすればよい。逆に、AXが100以上ならCXに1を加えたいなら、
	CMP AX,100
	SBB CX,-1
とすればよい。二番目の命令の即値を適当な値にすれば、1だけ異なる二つの数を場合分けによって加減算できる。

例えば、AXが100未満ならCXを30に、そうでなければ10にしたければ、

	CMP AX,100
	SBB CX,CX
	AND CX,20
	ADD CX,10
とすればよい。定数の値によっては、AND命令やADD命令は不要である。

今までの例では、CFをCMP命令でセットしたが、ADD,SUB命令や、ローテート/シフト命令などでもよい。特に、レジスタの値の符号によってCFを設定するときは、

0以上ならセット	CMP AX,8000H または SUB AX,8000H
負ならセット	SHL AX,1 または ADD AX,AX または ADD AX,8000H
などがあるので、レジスタの値を変えてよいか、どちらでセットされるのが都合よいかに応じて選ぶ。

CFを最上位桁として利用
加減算の結果をすぐに2で割るときは、CFを最上位桁として利用できる。例えば、AXとCXの符号なし平均をとるときは、
	ADD AX,CX
	RCR AX,1
とすればよい。

フラグの保存
フラグを設定する場所と使う場所の間にフラグを変化させる命令があるときには、フラグを保存する必要がある。普通はPUSHF,POPF命令を使う。AHが空いていて、OFを保存する必要がなければ、LAHF,SAHF命令を使うと、速く、何度も取り出せて便利である。AHが空いていなくても、例えばALが空いていて、CFだけ保存するのなら、 SBB AL,AL で保存して、 ROL AL,1 で取り出せばよい。80386と80486では、 ADD AL,AL で取り出したほうが速いが、8回までとなる。
		Bytes	8086	80286	80386	80486
PUSHF		1	10	3	4	4
LAHF		1	4	2	2	3
SBB AL,AL	2	3	2	2	1
		Bytes	8086	80286	80386	80486
POPF		1	8	5	5	9
SAHF		1	4	2	3	2
ROL AL,1	2	2	2	3	3
ADD AL,AL	2	3	2	2	1

86系CPUでは、MOV命令などの演算を行わない命令では、フラグは変化しない。これを利用すると、次のようなコードを書ける。

	TEST AX,AX
	MOV AX,100
	JZ L1
	MOV AX,200
L1:
演算を行っても、フラグを変化させない命令もある。INC,DEC命令はCFを変化させない。ROL,ROR,RCL,RCR命令は、PF,AF,ZF,SFを変化させない。また、NOT命令はすべてのフラグを変化させない。LEA命令も、フラグを変化させない演算命令として使える。

メモリ

メモリアクセスは時間がかかるので、なるべくまとめて行うとよい。例えば次のように、
A	DB ?
B	DB ?
二つのバイトデータAとBがメモリ上で並んでいるときは、
	MOV WORD PTR A,5*256+3 ; Aに3,Bに5
のようにまとめて初期化などをすることができる。

メモリ上のバイトデータをワードレジスタにゼロ拡張して入れたいことが頻繁にあるなら、バイトデータの次のアドレスに0を入れておく。例えば、

COUNT	DB ?
	DB 0
のように定義し、
	MOV COUNT,AL          ; バイトデータの書き込み
	MOV CX,WORD PTR COUNT ; ワードデータとして読み込み
のように使う。

初期化の必要なワークエリアは、大きな領域を規則的に埋める場合を除き、初期化のコードを書くより、データそのものを書いたほうがよい。例えば、10の冪のテーブルが必要な場合、

	MOV AX,1
	MOV DI,OFFSET ES:POW10
	MOV CX,5
L1:	STOSW
	MOV DX,AX
	SHL AX,1
	SHL AX,1
	ADD AX,DX
	SHL AX,1
	LOOP L1
などとするよりは、単に
POW10	DW 1,10,100,1000,10000
で十分である。テーブルを作るための計算が複雑で、暗算ではできないときには、別のプログラムを作って先に計算して使う。アセンブラのマクロを使って計算してもよい。

同時に使うことのない二つのワークエリアは、同じメモリ領域にとることができる。こうすると少ないメモリでプログラムを実行できるようになるし、80486以降ではキャッシュを有効に使うことになる。ただし、誤って同時に使ってしまわないように、常に気をつける必要がある。

プリフィックス

プリフィックスつきの命令は、8086では2クロックの追加となる。80286と80386では、命令のデコードと実行は独立したユニットで行われ、デコードされた命令はキューに蓄えられるため、プリフィックスのデコードにかかる時間が以前の命令の実行時間に隠れてしまうので、普通は追加クロックは必要ない。

プリフィックスのうち、セグメントオーバーライドプリフィックスは、プログラムの工夫でなくすことができる。多くの命令では、DSをデフォルトセグメントとして使うので、よく使うデータはなるべくDSでアクセスできるセグメントに置く。BPを含むアドレシングモードでは、デフォルトセグメントがSSになるので、DSとSSが同じセグメントを指しているとき以外は、DSの指すセグメントにあるデータをアクセスするのにBPを使ったアドレシングモードを使わないほうがよい。

セグメント

80286以前では、一つのセグメントの大きさは64Kバイトまでであり、また、セグメントレジスタは4つしかなくて役割が決まっているため、一度に扱えるデータ量は少ない。大量のデータを一度に扱うには工夫が必要である。

もし、速度的に重要な場所でセグメントの大きさを越えるデータを使うときには、セグメント内の処理を行うループの外に、セグメントレジスタを設定するコードを置くようにし、セグメントレジスタを頻繁にいじるのは避けるべきである。例えば、セグメントアドレスBXからBPパラグラフをクリアするには、次のようにする。

	XOR AX,AX
	MOV DI,AX
L1:	MOV ES,BX
	MOV CX,BP
	CMP CX,1000H
	JB L2
	MOV CX,1000H
	ADD BX,CX
L2:	SUB BP,CX
	SHL CX,1
	SHL CX,1
	SHL CX,1
	REP STOSW
	TEST BP,BP
	JNZ L1
速度的にそれほど重要でなければ、次のようにするのもよい。
	XOR AX,AX
L1:	MOV DI,AX
	MOV ES,BX
	MOV CX,8
	REP STOSW
	INC BX
	DEC BP
	JNZ L1

異なるセグメントにある何種類かのデータを同時に使いたいなら(例えば、二つまたは三つのデータに演算をして画面に書き込むような場合)、CSやSSの指すセグメントにデータを置いてもよい。逆に、データのあるセグメントの一部にコードを書いたり、スタックをとったりしてもよい。ただし、プロテクトモードでは、コードセグメントに書き込みができないので、CSを使ったデータアクセスでは読み出ししかできない。

データが128Kバイトまでなら、DSとESに分ける方法がある。例えば、MS-DOSの16ビットFATのように、高々2^16個の16ビットデータを扱う場合は、16ビットデータを上位と下位に分け、下位をDSの指すセグメントに、上位をESの指すセグメントに格納することができる。こうすれば、セグメントレジスタの値を変えずに全データをアクセスできる。

値の利用

値がわかっているレジスタは利用する。例えば、ループを抜けたとき、ループカウンタに使ったレジスタの値は0であることを利用できる。また、次のループ
	XOR AX,AX
	MOV [X],AX
L1:	;
	; 処理
	;
	MOV AX,[X]
	INC AX
	MOV [X],AX
	CMP AX,[X_LIMIT]
	JNE L1
では、「処理」の先頭でメモリ上のデータXの値がAXに入っていることを利用できる。

複数のレジスタに同じ値を入れたいときには、一つのレジスタに入れた値をMOV命令でコピーすればよい。バイトレジスタでなければ、コードが短くなる。8086ではクロック数も短縮される(バイトレジスタでも)。PentiumとPentiumProでは、依存関係が増えて並列度が下がるかもしれないので、注意する。

AXのmsbが0であるとわかっているとき、例えば直前にAXを0にした場合などで、DXを0にしたいときは、CDW命令を使うとバイト数を節約できる。

		Bytes	8086	80286	80386	80486
XOR DX,DX	2	3	2	2	1
CWD		1	5	2	2	3

アラインメント

奇数アドレスのワードデータをアクセスするには、メモリアクセスが2回必要になり、8086と80186で最低4クロック、80286で最低2クロックの追加サイクル(メモリアクセスのウェイトによる)が必要になる。ワードデータはなるべく偶数アドレスに置く(アラインする)ようにする。

80286以前では、命令のフェッチはワード単位で行われるので、よく使うジャンプ先は偶数アドレスに置いて、ジャンプ先の最初の命令全体がなるべく速くフェッチされるようにするとよい。アセンブラでは、EVEN擬似命令を使うと、その次の命令が偶数アドレスになるように、必要ならNOP命令を出力する。サブルーチンの先頭のラベルはこれで問題ないが、途中のラベルのときは、NOPの実行時間を節約するために、前の命令を故意に1バイト長くしたほうがよいこともある。例えば、次のコードでEVEN擬似命令がNOPを生成するなら、

	MOV AX,[SI+4]
	EVEN
L1:	MOV [DI],AX
代わりに
	DB 8BH,84H,04H,00H ; MOV AX,[SI+0004]
L1:	MOV [DI],AX
と書いたほうがよい。

時間のかかる命令を減らす

除算命令は、非常に時間のかかる命令で、使えるレジスタが限定されるので、速度が重要な場所ではなるべく使わないようにする。除数が2の冪なら、シフト命令を使う。そうでない場合でも、逆数の乗算に置き換える(文献2、文献5参照)。

乗算命令も、時間のかかる命令なので、速度が重要な場所では、MOVとシフトと加算の組合せに置き換える。例えば、AXを3倍するには

	MOV CX,AX
	SHL AX,1
	ADD AX,CX
とすればよいし、AXの3倍をBXに加えるには、 ADD BX,AX を3回繰り返せばよい。AXを257倍するなら MOV AH,AL である。80186以降では、80186で追加された、即値を含む3オペランドのIMUL命令(演算数と演算結果が同じビット数なので、フラグレジスタを無視すればMUL命令としても使える)を使うほうが便利なこともある。

時間のかかる演算の結果は、あらかじめ計算しておいて、メモリ上に置いておいたほうがよいこともある。例えば、円を描くための三角関数の値などは、メモリ上にテーブルを作っておいたほうがよい。

ストリング操作命令

80286以前では、ストリング操作命令を使うことは、プログラムを小さくし、かつ高速化する。ストリング操作命令は、プレフィクスを除けばすべて1バイト命令で、複数の処理をまとめて高速に行うからである。例として、二つのストリング操作命令、LODSB,STOSB命令と、それと同等な命令列を比較する。
			Bytes	8086	80286	80386	80486
LODSB			1	12	5	5	5
MOV AL,[SI] / INC SI	3	16	7	6	2
STOSB			1	11	3	5	5
MOV ES:[DI],AL / INC DI	4	19#	5	4	3#
# プリフィックスのクロックを含む

ストリング操作命令は、短い代わりに、使えるレジスタは固定されている。変えられるのは、方向(読み込みまたは書き込み後に、SIまたはDIを増やすか減らすかをDFで指定)と、転送元のセグメント(セグメントオーバーライドプリフィックスを使う)だけである。

リピートプリフィックスを使って繰り返し処理ができるのも、ストリング操作命令の有利な点である。ただし、8086では、セグメントオーバーライドプリフィックスと同時に使うと、うまく動作しないことがある(途中で割り込みがかかったときの処理にミスがあるため)。

REP STOS 命令や REP MOVS 命令で、たくさんのデータをストア/コピーするときには、なるべく大きいオペランドサイズを使うとよい。繰り返し回数が奇数の場合を考慮するなら、

	SHR CX,1
	REP MOVSW
	RCL CX,1
	REP MOVSB
のような命令列を使う。

XCHG命令

XCHG命令は、同時に二つのオペランドに書き込みのできる、便利な命令である(80486以降では、相対的に遅いので、あまり使わないほうがよい)。次のような応用がある。

クロック数が増えてもバイト数を減らしたいときは、AXから、またはAXへのMOV命令の代わりに、XCHG命令を使うこともある。

AAM,AAD命令

準備中

応用

準備中

パック演算

論理演算命令は、オペランドのすべてのビットを同時に操作する。これを利用して、8,16,または32(80386以降)個のデータをまとめて扱うことができる。例えば、SI,DIに入っている、16個の1ビットデータ2組を加算して、2ビットの和16個をAX(下位)とDX(上位)に得るには、次のようにする(半加算機)。
	MOV AX,SI
	XOR AX,DI
	MOV DX,SI
	AND DX,DI
この方法は、ビットマップデータの画像処理や、ライフゲームなどに応用できる。

ループ

while型(条件判断が先)のループは、例えば
L1:	CMP BYTE PTR [SI],0
	JZ L2
	INC SI
	JMP L1
L2:
のように実現できるが、もしループが何回も繰り返されるなら、
	JMP SHORT L4
L3:	INC SI
L4:	CMP BYTE PTR [SI],0
	JNZ L3
のようにすると、繰り返し中に実行されるJMP命令を減らすことができる。なお、この場合はさらに、JMP命令を逆の操作に置き換えて、
	DEC SI
L3:	INC SI
	CMP BYTE PTR [SI],0
	JNZ L3
とするとよい。80486以前なら、ジャンプの節で述べたように、
	DB 0A8H ; TEST AL,n
L3:	INC SI
	CMP BYTE PTR [SI],0
	JNZ L3
とするとよい。

逆に、ループが一度も繰り返されないことが多ければ、最初のままにするか、

	CMP BYTE PTR [SI],0
	JNZ L4
をその場に残し、L4で始まるループ本体を別の場所に置く。

80286以前では、同等な命令列と比べてJCXZ,LOOP命令が高速なので、ループカウンタにはなるべくCXを使い、JCXZ,LOOP命令を使うようにする。80386以降では、JCXZ,LOOP命令の使用はバイト数の節約にしかならず、特に80486以降では遅いので注意する。

			Bytes	8086	80286	80386	80486
TEST CX,CX / JZ ADDR	4	19/7	9+m/5	9+m/5	4/2
JCXZ ADDR		2	18/6	8+m/4	9+m/5	8/5
DEC CX / JNZ ADDR	3	19/7	9+m/5	9+m/5	4/2
LOOP ADDR		2	17/5	8+m/4	11+m	7/6

CX回繰り返すときには、

	JCXZ L6
L5:	;
	; 処理
	;
	LOOP L5
L6:
とすると、CXが0のときにも対処できる。CXを直前に計算したなどで、ZFにCXが0かどうかが入っているときは、
	; CXの計算
	JZ L8
L7	;
	; 処理
	;
	LOOP L7
L8:
とする。

ループ中で2つの値を交互に使いたい場合、XOR命令を使うと便利である。例えば、ALに3と6を交互に入れたいときには、ループ中に XOR AL,5 を入れる。ループ開始前のAXとDXを交互にAXに入れたいときには、ループの外に XOR DX,AX を置き、ループ中に XOR AX,DXを入れる。

3つあるいは4つの値を交代で使いたいときは、次のようにする。

このやり方をもっと多くの値に適用するのは、使うレジスタの数が線形に増えるのでうまくいかない。

速度的に重要なループは、完全に展開する(繰り返し回数が固定のとき)か、適当な回数(例えば8回)だけ展開しておいてそれを繰り返す(繰り返し回数が可変のとき)ようにし、繰り返すためのジャンプなどのオーバーヘッドを少なくするとよい。

ループを展開するとき、前後の処理をつなげて高速化できることがある。例えば、

	LODSB
	INC AL
	STOSB
	LODSB
	INC AL
	STOSB
の代わりに
	LODSW
	INC AL
	INC AH
	STOSW
を使う。ビットに関する操作では、8または16ビットぶんまとめるとだいぶ高速になることがある。このときは、バイトまたはワード境界を考慮してループを三つに分け、
  1. 最初の境界までのビットの処理
  2. バイトまたはワードごとの処理
  3. 残ったビットの処理
とするとよい。二番目の部分には、ストリング操作命令が使えることがある。

ループではないが、再帰呼び出しされるサブルーチンで、引数が1ずつ増加または減少するものも、引数について展開することがある。こうすると、引数のためのレジスタも節約できるし、引数に関する演算を先にすませることもできる。

自己改変コード

コードとデータは同じメモリに置かれているので、実行するコードをデータとみなして書き換えることができる。このように自分自身を変更するコードを、自己改変コードという。

書き換えた命令が既にフェッチされていた場合、書き換え前の命令が実行されてしまう。特に80386や80486では、プリフェッチキューが大きいので、このようなことが起こりやすい。対策は、命令を書き換えてから実行するまでの間に、IPを変更する命令を実行して、キューをフラッシュすることである。

書き換えて余った場所に何もしない命令を入れる必要があることがある。1バイトならNOPがよさそうだが、80386以前だと3クロックかかるので、フラグが変化してよいなら、2クロックですむCLCなどがよい。2バイトなら、 MOV AL,AL などがよい。3バイトなら、フラグが変化するが、 TEST AX,0 などがよい。

実行時コード生成

準備中

8087

準備中

複数のCPUで動くコード

複数の種類のCPUを対象にするときには、最も低レベルのCPUの持つ命令セットの範囲でコードを書く。普通は、コードが短くなるように、または最も低レベルのCPUで速くなるように最適化するが、特に高速にしたい場合には、CPUの種類を見分けて、それぞれのCPUに適したコードを実行するようにしたほうがよい。

CPUの種類を判別するには、次のようにする。

準備中

判別には時間がかかるので、判別結果はメモリに格納しておいて、必要な場所で場合分けに使う。判別したときにコードの一部を直接書き換えたり、実行時にコード生成したりする方法も使われる。


80386以降

この章では、80386とそれ以降のCPUについての最適化を説明する。80386では、32ビットのレジスタや、32ビットのデータを扱う命令が追加され、アドレシングモードも拡張されたほか、いくつかの命令が追加された。また、データバス幅も32ビットとなり(80386SXを除く)、命令フェッチなどが高速化された。80286と比べて、必要なクロック数が減少した命令がいくつかある(即値やメモリオペランドを持つ演算命令など)が、かえって増加した命令もある。

32ビット命令

32ビットのデータを扱う命令や、32ビットのアドレシングモードは、リアルモードや仮想8086モードでも利用できるため、積極的に使うべきである。例えば、16ビット命令では多倍長演算が必要だった32ビットの加減算を1命令にまとめられる。すると、16ビット命令では二つのレジスタが必要だったものが、一つの32ビットレジスタですむようになり、レジスタの利用効率も上がる。32ビットのアドレシングモードを使ってメモリをアクセスするときは、結果のオフセットアドレスが64K未満に収まるように注意する(リアルモードの特殊な状態を除く)。

特に32ビットとは関係ないコードでも、32ビット命令を使う価値はある。例えば、 REP MOVSW の代わりに REP MOVSD を使うと、高速化できる。ループの展開でも、32ビットぶんまとめて処理することで高速化が期待できる。また、4バイトまでのキーワード、例えば「ON」、「OFF」、「AUTO」などをCMP命令で直接比較することもできる。

32ビットアドレシングモードでは、ベースレジスタとインデックスレジスタの組合せがほぼ自由になったので、レジスタがアドレシングに使えるかどうか意識する必要はなくなった。次のようなアドレシングモードがある。

[base]			baseはEBP以外
[offset]
[base+disp]
[base+index*scale]	baseはEBP以外
[offset+index*scale]
[base+index*scale+disp]
baseはベースレジスタ(EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI)
indexはインデックスレジスタ(EAX,ECX,EDX,EBX,EBP,ESI,EDI)
scaleはスケール(1,2,4,8)
offsetは32ビットのオフセットアドレス
dispは8または32ビットの変位(定数)
注意するべき点は次の通りである。 また、各プロセッサ毎に、次の点に注意する。
80386
ベースレジスタとインデックスレジスタの両方を使うと、1クロック追加。
80486
インデックスレジスタを使うと、1クロック追加。即値を含む命令で、変位またはオフセットを使うと1クロック追加。
Pentium
即値を含む命令で、変位またはオフセットを使うと、ペアリング不可。
PentiumPro
なし

プリフィックス

80386では、80286以前からあるプリフィックスの他に、オペランドサイズプリフィックスと、アドレスサイズプリフィックスの二つが追加された。

16ビットモード(リアルモードと仮想8086モードを含む)で32ビットデータを扱う命令を使うと、命令の前に1バイトのオペランドサイズプリフィックスがつく。また、32ビットのアドレシングモードを使うと、命令の前に1バイトのアドレスサイズプリフィックスがつく。このため、16ビットモードで32ビットのデータやアドレシングモードを使うのは、32ビットが本当に必要な場合に限るべきである。

32ビットプロテクトモードでは、32ビットのデータやアドレシングモードを使ってもサイズプリフィックスはつかない。8ビットのデータもオペランドサイズプリフィックスを必要としない。逆に、16ビットのデータやアドレシングモードを使うと、プリフィックスがつく。だから、32ビットモードではなるべく16ビットのデータやアドレシングモードを使わず、8,32ビットのデータと32ビットのアドレシングモードを使うとよい。

80386以降のプロセッサで、プリフィックスつきの命令を使った場合の影響は、次の通りである。ここで、プリフィックスには、先頭バイトが0Fhの命令の0Fhも含む。

80386
デコードに追加クロックが必要になるが、たいていは命令の実行時間に隠れる。問題になるのはたいてい、ジャンプ先の最初の命令だけである。
80486
near条件ジャンプ以外は、デコード時間に1クロック追加される。前の命令が2クロック以上かかるときは、実行時間に隠れる。
Pentium
near条件ジャンプ以外は、デコード時間に1クロック追加される。前の命令ペアが2クロック以上かかるときは、実行時間に隠れる。また、ペアリングに制限ができる。
PentiumPro
0Fh以外はデコードに1クロック追加される。

追加された命令

32ビットへの符号拡張をするCWDE命令と、64ビットへの符号拡張をするCDQ命令が追加された。また、ゼロ拡張や符号拡張とMOV命令を複合した、MOVZX,MOVSX命令が追加された。これらは、80386では比較的高速で短く、便利である。MOVSX命令は、普通に MOVSX EAX,BYTE PTR [EBX] のように使えるほか、 MOVSX ECX,CX のように、EAX以外のレジスタに対するCWDE命令として使うこともできる。なお、 MOVZX EBX,BX を使うよりは、 AND EBX,0FFFFH または LEA EBX,[BX] を使うほうがよい。どちらもMOVZXより高速だが、AND命令はバイト数が多くなり、LEA命令は80486とPentiumでアドレス生成インターロックを受けるので、必要に応じて使い分ける。

任意のレジスタ同士、あるいはレジスタとメモリのIMUL命令が追加された。フラグレジスタを無視すれば、MUL命令としても使える。

SHLD,SHRD命令は、多倍長シフトのための命令だが、32ビットレジスタのデータを16ビットずつ二つに分けるために使える。例えば、 SHLD ECX,EDX,16 とすると、EDXの上位16ビットをECXに入れることができる(EDXは変化しないので、EDXの下位16ビットはDXにはいっている)。ただし、Pentiumでは、

	PUSH EDX
	POP DX
	POP CX
としたほうがよい。

逆に、二つの16ビットレジスタのデータをつなげて一つの32ビットデータにするには、SHRD命令を使うよりは

	SHL ECX,16
	MOV CX,DX
がよい。80486とPentiumでは、
	PUSH CX
	PUSH DX
	POP ECX
のほうがよい。

ビット操作命令(BT,BTR,BTS,BTC)では、ビット位置をレジスタまたは即値で直接指定できるため、ビット位置が可変のときのビット操作に便利である。次の例では、ビットのテストをしているが、BT命令を使った場合は結果がCFに入るので注意する。

BTR,BTS,BTC命令は、ビットのリセット、セット、反転を行うと同時に、変更前のビットの状態をCFに入れる。ビットテストが不要の場合でも、BTR,BTS,BTC命令を使うと、コードが短くなったりワークレジスタが必要なくなったりするので便利である。

レジスタに対するビット操作命令と、ビット位置が即値のビット操作命令では、ビット位置は下位4または5ビットのみが有効である。メモリに対する、ビット位置がレジスタ指定のビット操作命令は、全ビットが有効になるので、非常に便利である。そのアドレスのワードまたはダブルワードだけでなく、符号つき16ビットまたは32ビットのビット位置で指定される任意のメモリのビットを操作できる。例えば、DS:SIで始まるビットベクトルのビットCXをテストするには、

	MOV BX,CX
	SHR BX,3
	AND CL,7
	MOV AX,1
	SHL AX,CL
	TEST [SI+BX],AX
などとする必要はなく、 BT [SI],CX でよい。

80386には他にも追加された命令があるが、普通のプログラムではあまり使わない。

LEA命令

アドレシングモードが拡張されたため、LEA命令の有効性が上がった。例えば、ESPを除く任意のレジスタ二つと定数の加算を1命令で実現できる。スケールファクタを使うともう少し複雑な演算もできる。しかも、クロック数はレジスタ間の演算命令と同じか1クロック長いだけである。ただし、普通の演算命令と異なり、フラグは変化しない。なお、80486とPentiumでは、アドレス生成インターロックに注意する。
例:
	LEA ESI,[EBX+EAX*4+8] ; ESIにEBX+EAX*4+8を入れる
	LEA ESI,[EBX+(EAX+2)*4] ; 上と同じ
定数の乗算に応用することもできる。
	LEA EAX,[EAX+EAX*2] ; 3倍
	LEA EAX,[EAX+EAX*4] ; 5倍
	LEA EAX,[EAX+EAX*8] ; 9倍
乗数が2の冪なら、 LEA EAX,[EAX*8] などは使わずに、 SHL EAX,3 などのほうがよい。

16ビットモードでは、レジスタに定数を入れるときにLEA命令を使うと有利な場合がある。短い定数や、16ビットのオフセットを、32ビットレジスタに入れたい場合である。

ローテート/シフト

80386以降では、バレルシフタのおかげで、回数が1以外のローテート/シフト命令も、回数によらない一定時間で実行できるようになった(80486のRCL,RCR命令を除く)。そのため、例えばEAXの上位ワードと下位ワードを入れ替えるために ROL EAX,16 を利用するなど、たくさんの回数のローテート/シフトをためらわずに用いることができる。

80386では、RCL,RCR命令は遅くなったので、なるべく使わないほうがよい。80486以降でも、回数が1以外のRCL,RCR命令は遅いので、使わないほうがよい。

アラインメント

80286以前ではワードデータを偶数アドレスに置いたほうがよいのと同様に、80386以降では、ダブルワードデータを4で割り切れるアドレスに置いたほうがよい。なお、ワードデータを4で割って1余るアドレスに置いても、80386SX以外では遅れはない。アラインされていないデータ(ダブルワード境界をまたぐデータ)をアクセスすると、プロセッサ毎に次のような追加クロックが必要になる。
80386
メモリアクセスが2回必要になり、最低2クロックの追加サイクル(メモリアクセスのウェイトによる)が必要になる。
80486
データがキャッシュにある場合でも、3クロックの追加サイクルが必要になる。80486のキャッシュは一部を除いてライトスルー方式だが、書き込みのときも4段の書き込みバッファがあふれなければ3クロックの追加サイクルになる。書き込みバッファがあふれたり、データがキャッシュにない場合の読み込みでは、メモリアクセスの方式やクロックの倍率により異なる。
Pentium
追加サイクルは80486とほぼ同様である。キャッシュがライトバック方式になったので、キャッシュにあるデータの書き込みでは書き込みバッファを意識する必要はない。書き込みバッファは1段なので、キャッシュにないデータに書き込むときは注意する。
PentiumPro
データの読み出しがクワッドワード(8バイト)境界をまたぐと4クロックの追加サイクルが必要になる。キャッシュライン(32バイト)境界をまたぐとさらに遅れる。データの書き込みでは、キャッシュライン境界をまたぐときだけ追加サイクルが必要になる。

コードのアラインメントは、プロセッサによって事情が異なり、80386以降で共通するやり方はない。強いていうなら、80386は元々遅いのでアラインメントによる遅れの寄与は小さいとみなして、80486に合せておけばよいだろう。

80386
命令のフェッチはダブルワード単位に行われるため、ジャンプ先の最初の命令がなるべく最初のダブルワードに入るようにする。
80486
命令のフェッチはキャッシュから16バイト単位で行われるため、ジャンプ先の最初の命令が16バイト境界をまたがないようにする。またいだ場合は2クロックの遅れが生じる。最初の命令が1クロックで実行できるときには、次の命令も含めて16バイト境界をまたがないようにする。
Pentium
命令のフェッチはキャッシュラインの境界をまたいでも遅れないので、コードのアラインメントは必要ない。
PentiumPro
命令のフェッチはキャッシュから16バイト単位で行われ、3命令まで同時にデコードできるため、ジャンプ先の最初の3命令がなるべく16バイト境界をまたがないようにする。

リアルモードの特殊な状態

プロテクトモードからリアルモードに戻るときに、セグメントリミットを4GBに設定しておくと、リアルモードで32ビットアドレシングモードを使って、すべての物理メモリにアクセスすることができる。リアルモードでは普通、オフセットアドレスの範囲は0〜65535であり、これを越えるメモリアクセスは、擬似保護例外(INT 0Dh)を発生させる。しかし、次のようなコードを使って、DS,ES,FS,GSのセグメントリミットを4GBにしておけば、例外は発生せず、32ビットのオフセットアドレスを使えるようになる。
; GDTの定義
GDT_BASE LABEL BYTE
	DB 0,0,0,0,0,0,0,0
	DB 0FFH,0FFH,0,0,0,92H,8FH,0

SEL_NULL EQU 00H
SEL_FLAT EQU 08H
SEL_LIMIT EQU 10H

GDT_PNT LABEL FWORD
GDT_PNT_LIMIT DW SEL_LIMIT-1
GDT_PNT_BASE DW GDT_BASE,0

; GDT_PNTの設定
	XOR EAX,EAX
	MOV AX,CS
	SHL EAX,4
	ADD DWORD PTR GDT_PNT_BASE,EAX
	LGDT LARGE [GDT_PNT]
; プロテクトモードへ
	SMSW AX
	OR AL,01H
	LMSW AX
	JMP $+2
; セグメントの設定
	MOV AX,SEL_FLAT
	MOV DS,AX
	MOV ES,AX
	MOV FS,AX
	MOV GS,AX
; リアルモードへ
	MOV EAX,CR0
	AND AL,0FEH
	MOV CR0,EAX
	JMP $+2
; 次のコード

この状態を利用するメリットは、次の通りである。

しかし、次のような欠点もある。 そのため、プロテクトモード用の開発ツールが使える場合は、これの利用はお勧めできない。

80486以降

この章では、80486(DX4を含む)に特有の最適化を説明するが、多くは以降のCPUにもあてはまる。80486は、MOV命令やレジスタ同士の演算命令の大部分を1クロックで、nearジャンプを3クロックで実行できるなど、実行速度が向上した。内蔵された8KBまたは16KBのキャッシュメモリがこの高速性を生かしている。外部メモリアクセスに使われるクロックスピードの2倍(DX2,SX2)または最大3倍(DX4)で命令を実行するのにも内蔵キャッシュが役立っている。80386以前では別のチップになっていた浮動小数点演算ユニットも内蔵され(SXとSX2を除く)、高速になった。

BSWAP命令

80486で追加された、BSWAP命令は、32ビットレジスタのバイト順を反転する(ワード順の反転にはROL命令を使う)。本来はバイト順が逆のデータフォーマットでのデータ交換用の命令と思われるが、PC-9800シリーズのグラフィックV-RAMのように、バイト内の上位のビットが画面の左に対応するような場合にも使える。例えば、V-RAMのダブルワードのうち左のCLビットを0にしたければ、
	MOV EAX,0FFFFFFFFH
	SHR EAX,CL
	BSWAP
のようにして作ったマスクパターンを使えばよい。

32ビットのビット逆順に使うこともできる。共通/80286以前のビット逆順の方法をそのまま32ビットにすると21命令必要だが、BSWAP命令を使えば17命令ですむ。

	MOV ECX,EAX
	AND EAX,55555555H
	XOR ECX,EAX
	ROL EAX,2
	OR EAX,ECX
	MOV ECX,EAX
	AND EAX,66666666H
	XOR ECX,EAX
	ROL EAX,4
	OR EAX,ECX
	MOV ECX,EAX
	AND EAX,78787878H
	XOR ECX,EAX
	ROL EAX,8
	OR EAX,ECX
	ROR EAX,7
	BSWAP EAX

遅い命令

80486で高速化されなかった命令は、なるべく使わないほうがよい。

リピートプリフィックスなしのストリング操作命令は遅いので、速度が要求されるところではMOV命令とINC命令などの組合せに置き換えたほうがよい(ストリング操作命令を参照)。そうすれば、Pentiumではペアリングも可能になる。

リピートプリフィックスつきのストリング操作命令は、繰り返し回数が少ないときのオーバーヘッドが大きいので、注意して使う。80486ではREP LODSとREP STOS、PentiumではREP LODSとREP SCASとREP CMPSは、普通の命令の組合せに置き換えたほうが速い。

JCXZ,LOOP命令などは遅いので、普通の命令の組合せに置き換えたほうが速い。

MOVZX命令は高速化されなかったので、先にレジスタ全体をクリアしてからMOV命令を使うほうがよい。そうすれば、Pentiumではペアリングも可能になる。

シフト命令は高速化されなかったので、なるべく使わないほうがよい。ローテート/シフトで説明した、SHL→ADD、RCL→ADCの置き換えも利用するとよい。Pentiumでは一部を除いて高速化されたので、使ってもよい。

RCL,RCR命令を除いて、即値のローテート/シフトは、即値が1以外のためのエンコーディングのほうが速いので、 SHR EAX,1 を SHR EAX,21H で置き換える(80186以降では、シフトカウントは下位5ビットのみ有効)などして利用するとよい。

SAHF命令は2クロックかかるので、ビットを直接調べたほうが速い。Pentiumではペアリングも可能になる。80386でも高速化されるが、寄与は小さい。

				Bytes	80386	80486
SAHF / JC L1			3	10+m/6	5/3
TEST AH,1 / JNZ L1		5	9+m/5	4/2

キャッシュ

80486の高速性にとって、キャッシュは重要である。もしキャッシュを無効にすると、内部クロックの倍率にかかわらず、外部クロックの同じ80386と同程度の速度(本来の速度の1/2〜1/6)になってしまう。だから、プログラムの重要な部分ではキャッシュのふるまいを考慮したほうがよい。

80486は、8KB(DX4は16KB)の命令データ混在型のキャッシュメモリを内蔵している。データがキャッシュにのっていれば、1クロックで読むことができる。また、命令はキャッシュから16バイト単位でフェッチされる。命令がキャッシュにのっている限り、命令のフェッチ時間を考慮する必要はほとんどない(考慮が必要な点はアラインメントを参照)。

80486のキャッシュは、 write back enchanced 486DX2 を除いてライトスルー型で、キャッシュにのっているデータに書き込むと、メモリにも書き込まれる。4段の書き込みバッファがあるため、バッファに余裕がある限り書き込みも見かけ上1クロックで実行できる。命令とデータがすべてキャッシュにのっていれば、メモリに書き込む命令を実行しない限り、CPUからメモリへのアクセスは発生しないし、書き込む命令を続けて実行してバッファがいっぱいにならない限り、メモリへのアクセスで実行が待たされることもない。

80486のキャッシュは、16バイトのライン512個(DX4は1024個)から成る。各キャッシュラインは、16で割り切れる物理アドレスから始まる連続する16バイトのデータに対応する。キャッシュされていないデータを読むときには、CPUはキャッシュライン全体をメモリから読む。このとき、キャッシュラインのうちアドレスの小さいほうから読むのではなく、必要なデータを含むダブルワードから順に読む。

キャッシュラインは任意の物理アドレスのデータを保持できるわけではない。80486のキャッシュは 4-way set associative 型で、4個のキャッシュラインのセット128個(DX4は256個)で構成されている。各物理アドレスに対して割り当てられる可能性のあるキャッシュラインは1セット中の4個だけである。どのセットが使われるかは、物理アドレスのビット4〜10(DX4は4〜11)で決まる。物理アドレスの残りのビット(11〜31または12〜31)は各キャッシュラインに記憶される。

キャッシュの置換アルゴリズムは、次の通りである。各セットは、どのラインが新しいかを、3つのビットを使って記録している。4つのラインをA,B,C,Dとすると、一つはAまたはBとCまたはDのどちらが最近使われたかを、一つはAとBのどちらが最近使われたかを、一つはCとDのどちらが最近使われたかを記録している。キャッシュラインを置き換えるときには、この情報に基づいて古いほう捨てる。そのため、最も古いものではなく二番目に古いものを捨てることがある。

キャッシュミス時のメモリアクセス順の工夫

80486とPentiumは、読み出したいデータがキャッシュにのっていなかったなら、必要なデータを含むダブルワードあるいはクワッドワードから順にメモリを読み出して、キャッシュライン全体を満たす。データは最初のダブルワードまたはクワッドワードが読み出された時点で(Pentiumでは少し遅れがある)使用可能になる。

この後、キャッシュラインが満たされる前に、同じキャッシュライン中のデータをアクセスした場合、マニュアル上ではキャッシュライン全体を満たすまで待たされることになっているが、実際にはもっと長く(2倍程度)待たされる。この余計な待ち時間は、メモリアクセスの順番を工夫することで、なくすことができる。データをアドレス順に読み出している場合には、次のキャッシュライン中のデータに一度アクセスして、確実に現在のキャッシュラインを満たしてから、現在のキャッシュライン中のデータを順次処理すればよい。

例えば、80486の場合、ESIにダブルワードの配列のオフセット(キャッシュライン境界にアラインされているとする)、ECXに要素数/4がはいっているとき、

L1:	ADD EAX,[ESI]
	ADD EAX,[ESI+4]	; ここで余計に待たされる
	ADD EAX,[ESI+8]
	ADD EAX,[ESI+12]
	ADD ESI,16
	DEC ECX
	JNZ L1
のような処理は、
	ADD EAX,[ESI]
	DEC ECX
	JZ L2
L1:	ADD EAX,[ESI+16]
	ADD EAX,[ESI+4]
	ADD EAX,[ESI+8]
	ADD EAX,[ESI+12]
	ADD ESI,16
	DEC ECX
	JNZ L1
L2:	ADD EAX,[ESI+4]
	ADD EAX,[ESI+8]
	ADD EAX,[ESI+12]
とすると、余計に待たされることがない。

追加クロックが必要な命令

near条件ジャンプを除いて、プリフィックスつきの命令のデコードには、プリフィックス一つあたり1クロック余計にかかる(プリフィックスを参照)。80486は、デコードされた命令を蓄えるキューを持っていないので、直前の命令が1クロックで実行されたときには、プリフィックスつきの命令の実行開始が余計なデコード時間のぶん遅れる。直前の命令が2クロック以上かかったときには、このデコードの遅れは隠れてしまう(Nクロックかかる命令は、N-1クロックの余計なデコード時間を隠す)。

インデックスレジスタを使う命令は、1クロック余計にかかる。また、オフセットと即値の両方、または変位と即値の両方を使う命令も、1クロック余計に時間がかかる。また、この後述べる部分レジスタストールアドレス生成インターロックによって余計なクロックが必要になることもある。この時間も続く命令のデコードの遅れを隠すのに使える。

部分レジスタストール

80486では、レジスタのある部分に書き込んだ後、レジスタの別の部分を読み出すと、1クロック余計にかかることがある。これを、部分レジスタストールという。例えば、次の命令列
	XOR AH,AH
	INC AX
は2クロックではなく3クロックかかる。どの組合せでストールが発生するかを、EAXレジスタを例にして示す。
Write	Read	ストール
AL	AH	1
AH	AL	1
AL	AX	1
AH	AX	1
AL	EAX	1
AH	EAX	1
AX	EAX	1
AX	AL	0
AX	AH	1
EAX	AL	0
EAX	AH	1
EAX	AX	0

Pentiumでは、部分レジスタストールは発生しない。

PentiumProでは、レジスタの一部に書き込んだ後、全体を読み出すと、その読み出す命令が5クロックの間ストールする。80486とは異なり、例えばALに書き込んだ後AHを読み出してもストールは発生しない。

アドレス生成インターロック

80486とPentiumでは、メモリアドレスの指定に使われているレジスタの値を、直前の命令で変更すると、命令の実行開始が1クロック遅れる。これをアドレス生成インターロックという。例えば、次の命令列
	ADD EBX,4
	MOV EAX,[EBX]
	INC ECX
は、80486の場合、各命令のクロック数の合計の3クロックではなく、4クロックかかる。間に命令をはさんで、
	ADD EBX,4
	INC ECX
	MOV EAX,[EBX]
とするか、メモリアクセスを先に行って、
	MOV EAX,[EBX+4]
	ADD EBX,4
	INC ECX
とすれば、3クロックで実行できるようになる。

80486では、直前に変更したレジスタが、全体ではなく部分レジスタの場合、部分レジスタストールと合わせて、合計2クロックの遅れが生じる。ここで注意するべきことは、16ビットアドレシングモードを使った場合でも、32ビットぶん使っているかのように遅れることである。つまり、アドレシングモードが16ビットか32ビットかにかかわらず、直前に変更したレジスタが32ビットレジスタなら遅れは1クロックですむが、8または16ビットレジスタなら2クロックになる。

PUSH,POP,CALL,RET命令は、SPまたはESPレジスタを使ったメモリアクセスを行うので、直前の命令でSPまたはESPを変更した場合には、アドレス生成インターロックを受ける。ただし、80486では、CALL命令は遅れない。

PUSH,POP,CALL,RET命令はまた、SPまたはESPレジスタを変更するが、80486とPentiumは専用の回路を持っており、続けてPUSH,POP,CALL,RET命令を使ったり、メモリアドレスの指定にESPを使ったりしても、遅れはない。ただし、PentiumでRET命令に即値オペランドがある場合に限り、次の命令でアドレス生成インターロックが発生する。


Pentium

この章では、Pentiumに特有の最適化を説明する。PentiumにはMMX対応のものとそうでないものがあるが、ここでは主にMMXなしのものを扱う。Pentiumは二つのパイプラインを持ち、ある条件のもとで命令を二つ同時に実行できる。また、内蔵キャッシュメモリは命令キャッシュとデータキャッシュに分かれ、それぞれ8KB(MMX対応は16KB)ある。データキャッシュはライトバック方式で、二つの命令が同時に書き込むことができる。さらに、分岐予測機構を持ち、正しく予測できたときには、ジャンプ命令を1クロックで実行できるようになった。内蔵されている浮動小数点ユニットは、パイプライン動作が可能になり、演算結果が出るまでの間に他の命令を受け付けられるようになった。

ペアリング

Pentiumは、命令を実行するパイプラインを二つ持っており、以下で述べる条件のもとで、命令を二つペアにして同時に実行できる。これをペアリングという。二つのパイプラインはUパイプ、Vパイプと呼ばれ、命令を二つ同時に実行するときには、先の命令がUパイプで、後の命令がVパイプで実行される。どちらかの命令の実行が先に終了しても、次の命令の実行は始まらず、両方の命令の実行が終わってからになる。Pentiumの最適化では、命令がうまくペアになるようにし、二つのパイプラインが休まずに命令を実行するようにすることが重要である。

ペアにできる命令は次の通りである。

U,Vどちらのパイプでもペアにできる
Uパイプでのみペアにできる
Vパイプでのみペアにできる
ここで、rは汎用レジスタ、mはメモリ、iは即値、accはEAX/AX/ALを表す(正確には命令一覧を参照)。TEST命令のうち、 TEST r/m,i の形式(rがEAX/AX/ALのときを除く)はペアにできないので注意する。

複雑そうだがペアにできる命令としては、LEA命令と、CALL(near直接)命令が挙げられる。逆に、ペアにできそうでできない命令としては、NEG,NOT,BSWAP命令と、TEST命令の一部の形式が挙げられる。

連続する二つの命令がペアになる条件は次の通りである。

文献3には、7バイトを越える命令はペアにできないと書かれているが、プリフィックスなしでは上の条件のほうが強く、7バイトの命令にプリフィックスをつけて8バイトにしてもペアにできたので、これは誤りであろう。もしかすると、文献4のPrefixesの章(HTML版日本語訳ではプリフィックスの章)に書かれている、MMX対応PentiumのFIFOバッファの制限を簡略化して説明したのかもしれない。

ペアになった命令の実行は、普通は、クロック数が多いほうの命令のクロック数ぶんかかる。ただし、ある場合には、余計なクロックを必要とすることがある。

ペアリングを有効に使うためには、次のようなことに注意する。

キャッシュ

Pentiumの内蔵キャッシュメモリは、命令キャッシュとデータキャッシュに分かれていて、それぞれ8KB(MMX対応は16KB)ある。データキャッシュはライトバック方式になっていて、メモリへの書き込みはキャッシュの置換の際に行われる。書き込みたいデータがキャッシュにのっていなかったときは、そのままメモリに書き込む。また、データキャッシュは、アドレスのビット2〜4で決まる、8つのキャッシュバンクに分かれており、異なる二つのバンクには、同時にアクセスすることができる。このため、ペアで実行される二つの命令がともにメモリアクセスをすることができる。

MMXなしPentiumでは、命令キャッシュとデータキャッシュはともに、32バイトのラインサイズで、2-way set associative型である。そのため、80486よりキャッシュの競合が起きやすい。MMX対応Pentiumでは、どちらも4-way set associateve型に変更されている。

自己改変コードに対応するため、命令キャッシュにあるメモリアドレスに書き込む命令を実行すると、そのキャッシュラインは無効にされる。もし命令がデコードされていたら、それも無効にされる。命令は改めてメモリから(データキャッシュからではない)読み込まれる。

実際には、メモリからの読み出しでも命令キャッシュのキャッシュラインは無効にされ、また、一つ前のキャッシュラインも無効にされる。そのため、命令を含むキャッシュライン(32バイト単位)、またはそれに続く32バイト中にあるデータにアクセスすると、次にその命令を実行するときにメモリアクセスが発生することになり、実行速度がかなり低下してしまう。命令とデータはなるべく番地を離して置くようにするとよい。

追加クロックが必要な命令

Pentiumのデータキャッシュは、アドレスのビット2〜4で決まる、8つのキャッシュバンクに分かれており、同じバンクに同時にアクセスすることはできない。MOVS命令では、データの書き込みと次のデータの読み込みが同時に行われ、CMPS命令では、二つのデータの読み込みが同時に行われていると推測される。そのため、転送/比較元と先のアドレスの差の下位5ビットによって、繰り返し1回あたり次のように追加クロックが必要になる。
	-4	-3	-2	-1	0	1	2	3	4
MOVSB	0	0	0.25	0.5	0.75	1	0.75	0.5	0.25
MOVSW	0		0		0.5		1		0.5
MOVSD	0				0				1
CMPSB	0	0.25	0.5	0.75	1	0.75	0.5	0.25	0
CMPSW	0		0.5		1		0.5		0
CMPSD	0				1				0

ペアリングのために、最大2命令をはさんでアドレス生成インターロックが発生する場合がある。

Pentiumには、部分レジスタストールはない。文献3には、「レジスタが書き込まれたときと同じ境界で読み込めない場合は、ストールが発生する。これは、AH/EAX、BH/EBX、CH/ECX、DH/EDXのレジスタの組み合わせに該当する」とあるが、実際にはストールは観察されなかった。

分岐予測

Pentiumの分岐予測機構についての詳細は、文献4のjumps and branchesの章(HTML版日本語訳ではジャンプとブランチの章)を見てほしい。これには、Branch Target Bufferの先読みなど、普通の文献では得られない詳しい情報が書かれている。

ここでは、MMXなしPentiumの分岐予測と予測ミスのペナルティーについて簡単に説明する。分岐予測の対象になるのは、IPまたはEIPを変更するすべての命令である(INTやIRETも含むか?)。分岐命令の実行が始まる前に、Branch Target Bufferに登録されている分岐先のアドレスにある命令(分岐しないと予測したときは次の命令)のフェッチとデコードを始めることで、分岐命令は高速に実行される。予測が失敗すると、フェッチとデコードをやり直すので、次の命令の実行開始まで時間がかかる。

このため、次の点に注意してコードを書くとよい。


PentiumPro以降

準備中

追加された命令

CMOVcc FCMOVcc FCOMI/FCOMIP
	FSTSW AX
	SAHF
の置き換えとして利用できる。

キャッシュ

PentiumProのデータキャッシュは、アドレスのビット3と4で決まる4つのキャッシュバンクに分かれており、キャッシュバンクが重ならなければ、メモリの読み出しと書き込みを同時に行うことができる。 PentiumProでは、書き込みたいデータがキャッシュにのっていなかったときは、読み出しのときと同様にキャッシュラインを満たして、キャッシュに書き込む。 レベル2キャッシュは、命令・データ混合型の4-way set associativeキャッシュである。

ストール

PentiumProでは、レジスタの一部に書き込んだ後、全体を読み出すと、その読み出す命令が5クロックの間ストールする。ストール中でも、依存関係のない他の命令を実行することはできるが、5クロックをうめるのは難しい。それよりは、部分レジスタを使わなくてすむようにコードを変更したほうがよい。その際、PentiumProではMOVZXや乗算命令が高速化されていることを、利用するとよい。例えば、

	MOV AL,[ESI]
	MOV AH,AL
	SHL EAX,8	; ストール
	MOV AL,AH
	MOV [EDI],EAX	; ストール
は、MOVZXとシフトを使って、
	MOVZX EAX,BYTE PTR [ESI]
	MOV ECX,EAX
	SHL EAX,16
	MOV EDX,ECX
	SHL ECX,8
	OR EAX,EDX
	OR EAX,ECX
とするか、IMULを使って、
	MOVZX EAX,BYTE PTR [ESI]
	IMUL EAX,10101H
	MOV [EDI],EAX
とするとよい。

特別な場合として、SUB命令またはXOR命令でレジスタ全体をクリアしてから部分レジスタ(AH,CH,DH,BHを除く)を変更した場合は、ストールは発生しない。Pentium以前の、MOVZX命令が低速なCPUでも同じコードを実行したい場合、 MOVZX AL,[ESI] の代わりに

	XOR EAX,EAX
	MOV AL,[ESI]
を使うなどして、どのCPUでもひどく遅くならないコードにすることができる。

なお、ALとAHのような組み合わせでは、ストールは発生しないばかりか、同時にアクセスすることもできる。もちろん、書き込んだデータの一部を読み出す場合には、ストールは発生しない。

PentiumProではまた、メモリに書き込んだデータと書き込む前のデータを合成して読む必要があるときには、8クロックの間ストールする。例えば、

	MOV [EBX],AL
	MOV ECX,[EBX]
あるいは、
	MOV [EBX],EAX
	MOV ECX,[EBX+2]
のような場合である。また、
	MOV [EBX],EAX
	MOV AL,[EBX+2]
のように、同じデータを異なるアドレスを使ってアクセスする場合にも、8クロックの間ストールする。

PentiumProではこの他に、FLAGSに関するストールにも注意する必要がある。次のような命令では、4〜5クロックのストールが発生する。

  1. INC命令(CFを変更しない)の後の、CFを使う命令(JC,ADCなど)
  2. ROL,ROR命令(PF,ZF,SFを変更しない)の後の、PF,ZF,SFを使う命令(JZなど)
  3. SAHF命令(FLAGSの下位8ビットのみを変更)の後の、FLAGSの下位8ビットとそれ以外の両方を使う命令(JLなど)
特に、1.は便利でよく利用されるので、要注意である。他のフラグも変更する必要がないなら、LEA命令で置き換えるとよい。

Pentium4

準備中
二つある2倍速のALUは、スタガードALUと呼ばれ、下位16ビットと上位16ビットを0.5クロックずらして演算することで、速度を稼いでいる。その代わり、INC,DECは少し遅く、シフトはかなり遅くなり、ADC,SBBに至っては禁止的に遅い。
ADD EAX,ECX / ADC EDX,0
ADD EAX,ECX / LEA EBX,[EDX+1] / CMOVC EDX,EBX

トレースキャッシュ

分岐ヒント

ストアフォワーディング


高速化の例

この章では、3x+1の問題(角谷の問題、コラッツの問題ともいう)を例にして、アセンブラでの高速化を解説する。なお、このコードを使った計算は現在休止中である。

問題

3x+1の問題とは、任意の自然数nに次の操作: を繰り返したときに、いつか必ず1になるかという問題である。理論的には未解決であるが、コンピュータを使った計算では、かなり大きな数まで成り立つことが確認されている。

n=1,2,3,…と1から順に確認するのなら、1になるまで繰り返さなくても、nより小さくなるまで繰り返せばよい。Pascal風に書くと、次のような操作を各nについて行えばよい。

x:=n; s:=0;
repeat
  s:=s+1;
  if odd(x) then x:=x*3+1
            else x:=x div 2
until x<n
ここでは、何ステップでnより小さくなったかを、変数sを使って数えている。ところで、3倍して1を足した後は必ず2で割ることになるので、まとめて計算すると、少し速くなる。途中結果がなるべく小さくなるように変形すると、次のようになる。
x:=n; s:=0;
repeat
  s:=s+1;
  if odd(x) then begin s:=s+1; x:=(x div 2)*3+2 end
            else x:=x div 2
until x<n

他にも、nが偶数のときは省いてよいなど、計算の手間を減らす方法がいろいろあるが、本題からはずれるので省略する。

アセンブリ言語へ

ではこれを、アセンブリ言語(80386以降とする)に直してみよう。まず、演算精度について考える。32ビット演算では、40億程度までしか計算できず、すぐに終わってしまう。64ビットあれば、兆(10^12)や京(10^16)のオーダーまで計算できそうである。ところが実際には、230億程度のnでも、途中結果xが64ビットを越えて、計算できなくなってしまう。そこで、64ビットを越えたら96ビット演算ルーチンに移行するようにする。Pascalコード中には現れていないが、x:=(x div 2)*3+2の計算中にオーバーフローをチェックすることにする。

64ビットの演算ルーチンでは、レジスタの割り当ては次のようにすればよいだろう。

EDI:ESI	n
EDX:EAX	x
EBP	s
EBX,ECX	作業用
それでは順にアセンブリ言語に直していこう。
x:=nは
	MOV EAX,ESI
	MOV EDX,EDI
s:=0は
	XOR EBP,EBP
s:=s+1は
	INC EBP
となる。if文の条件は、EAXの最下位ビットを調べて
	TEST AL,01H
	JZ Z2
とすればよい。then節中のs:=s+1は前と同じである。

x:=(x div 2)*3+2は、

	SHR EDX,1	;2で割る
	RCR EAX,1
	MOV ECX,EAX	;3倍
	MOV EBX,EDX
	ADD EAX,EAX
	ADC EDX,EDX
	ADD EAX,ECX
	ADC EDX,EBX	;*
	ADD EAX,2	;2を足す
	ADC EDX,0	;*
とすればよいのだが、よく見ると2で割ったものを2倍しているところがある。しかも、演算がオーバーフローしたかどうかのチェックが2回(*をつけた命令の後)必要である。最初にEDX:EAXをある定数と比較すれば、チェックは1回ですむが、それでも命令数は増えてしまう。x:=(x div 2)+1+xと変形して、
	MOV ECX,EAX
	MOV EBX,EDX
	SHR EDX,1
	RCR EAX,1	;EDXは80000000h未満
	ADD EAX,1
	ADC EDX,0	;オーバーフローは起きない
	ADD EAX,ECX
	ADC EDX,EBX	;*
とすれば、2命令短くなる上に、オーバーフローのチェックは最後だけでよい。チェックは、ラベルL23に96ビット演算への移行処理を用意し、
	JC L23
とする。

else節のx:=x div 2は、

	SHR EDX,1
	RCR EAX,1
でよい。

if文をまとめると、

	TEST AL,01H
	JZ Z2
	INC EBP
	MOV ECX,EAX
	MOV EBX,EDX
	SHR EDX,1
	RCR EAX,1
	ADD EAX,1
	ADC EDX,0
	ADD EAX,ECX
	ADC EDX,EBX
	JC L23
	JMP E2
Z2:	SHR EDX,1
	RCR EAX,1
E2:
となる。ここで、JMP命令を減らせないか考える。else節はxを2で割るだけで、これはthen節にもでてくるので、うまくまとめられれば、JMP命令は不要になる。そこで、これを分岐の前に移動する。2で割った後には余りがCFにはいるので、TEST命令は不要になる。
	SHR EDX,1
	RCR EAX,1
	JNC E2
	…
E2:
…の部分ではx:=(x div 2)+1+xの計算をしたいので、2で割る前のxの値も保存する(そうしないと、x:=(x div 2)*3+2の計算をすることになって遅くなる)ために、次のように書き換える。
	MOV ECX,EAX
	MOV EBX,EDX
	SHR EDX,1
	RCR EAX,1
	JNC E2
	INC EBP
	ADD EAX,1
	ADC EDX,0
	ADD EAX,ECX
	ADC EDX,EBX
	JC L23
E2:
xが偶数のときには少し冗長だが、TESTとJMPがなくなったことで補えるだろう。このコードにはまだ改良の余地がある。EDX:EAXに1を加えてからECX:EBXを加えている部分である。ここはJNC命令の後で、INC命令はCFを変えないので、CFがセットされていることを利用して、4命令を2命令に減らすことができる。オーバーフローのチェックを変える必要はない。これでif文は次のようになる。
	MOV ECX,EAX
	MOV EBX,EDX
	SHR EDX,1
	RCR EAX,1
	JNC E2
	INC EBP
	ADC EAX,ECX
	ADC EDX,EBX
	JC L23
E2:

実はまだ、改良できる。Pascalで書いたときからある冗長性なのだが、then節を通ったときはxの値が増加するので、次の処理であるnとの比較は無駄で、単に次の繰り返しに入ればよい。L2をループの最初のラベルとすると、

	JC L23
の代わりに
	JNC L2
	JMP L23
とすればよい。96ビット演算で、ESI:EDX:EAXにxを割り当てるとすると、L23の処理は、E3を96ビット演算ルーチンの入口として、
L23:	MOV ESI,1
	JMP E3
のようになる。これもまとめて、
	JNC L2
	MOV ESI,1
	JMP E3
とすると、JMPを節約できる。

最後の、until x<nの部分は、多くの場合上位ワードの比較だけですむと予想して、次のようにする。

E2:	CMP EDX,EDI
	JA L2	;L2はループの最初のラベル
	JB X2
	CMP EAX,ESI
	JAE L2
X2:

以上をまとめると、次のようになる。

	MOV EAX,ESI
	MOV EDX,EDI
	XOR EBP,EBP
L2:	INC EBP
	MOV EBX,EDX
	MOV ECX,EAX
	SHR EDX,1
	RCR EAX,1
	JNC E2
	INC EBP
	ADC EAX,ECX
	ADC EDX,EBX
	JNC L2
	MOV ESI,1
	JMP E3
E2:	CMP EDX,EDI
	JA L2
	JB X2
	CMP EAX,ESI
	JAE L2
X2:

Pentium向けの最適化

今までは、80386以降の一般的な最適化を行ってきたが、今度はこのコードを、Pentium向けに最適化することにする。コード中のどの命令もPentiumでは1クロックで実行でき(予測ミスしたジャンプを除く)、アドレス生成インターロックもないので、主にペアリングについて考える。最初の3命令はループの外なので無視し、L2以降の命令を順番に見ていこう。

最初のINC命令とMOV命令はペアになる。次のMOV命令とSHR命令は、SHR命令がUパイプでのみペアになるため、ペアになれない。MOV命令は単独で実行される。続くRCR命令もUパイプでのみペアになるので、SHR命令も単独で実行される。RCR命令とJNC命令は、フラグレジスタの特例によりペアになる。この部分は、INC命令をSHR命令とRCR命令の間にはさめば(INC命令がCFを変えないことを利用)、すべてペアにすることができる。

L2:	MOV EBX,EDX
	MOV ECX,EAX
	SHR EDX,1
	INC EBP
	RCR EAX,1
	JNC E2

次のINC命令とADC命令は、ADC命令がUパイプでのみペアになるため、ペアになれない。次のADC命令とJNC命令はペアになれる。INC命令がCFを変えないこと再び使って、二つのADC命令の間にはさめば、すべてペアにすることができる。

	ADC EAX,ECX
	INC EBP
	ADC EDX,EBX
	JNC L2

次のMOV命令とJMP命令はペアになるが、ここは上のJNC命令が予測ミスしたときに通るので、必ず予測ミスする(分岐予測を参照)。JMP命令が最初のペアに入らないように、NOPを挿入してもよいが、次のように MOV ESI,1 を2命令に分けて、合計のバイト数を減らすことにする。

	XOR ESI,ESI
	INC ESI
	JMP E3
XOR命令は単独で、INC命令はJMP命令とペアで実行される。

次のCMP命令とJA命令もペアになるが、 JNC E2 のジャンプ先なので、これで予測ミスが起きると必ず予測ミスする(分岐予測を参照)。 JNC E2 では予測ミスが起きやすいので、ペナルティーを頻繁に受けることになる。これもNOPを挿入する代わりに、CMP命令とSBB命令を使って、64ビットの大小比較をすることにする。

E2:	MOV EBX,EDX
	CMP EAX,ESI
	SBB EBX,EDI
	JNC L2
これはすべてペアになる。

以上の改良の結果は次のようになる。ループ中の命令が実行されるパイプラインを、コメントのU,Vで示す。

	MOV EAX,ESI
	MOV EDX,EDI
	XOR EBP,EBP
L2:	MOV EBX,EDX	; U
	MOV ECX,EAX	;   V
	SHR EDX,1	; U
	INC EBP		;   V
	RCR EAX,1	; U
	JNC E2		;   V
	ADC EAX,ECX	; U
	INC EBP		;   V
	ADC EDX,EBX	; U
	JNC L2		;   V
	XOR ESI,ESI	; U
	INC ESI		; U
	JMP E3		;   V
E2:	MOV EBX,EDX	; U
	CMP EAX,ESI	;   V
	SBB EBX,EDI	; U
	JNC L2		;   V
オーバーフローが起きず、分岐予測が成功した場合、xが奇数でも偶数でも、ループ1回あたり5クロックで実行される。

GCCとの比較

この程度の最適化なら、Pentium向けのコードを生成するコンパイラを使えば簡単にできると思う人がいるかもしれない。それでは、Pentium向けの最適化機能を持つGNU C Compilerと比較してみよう。

次の関数は、この章の始めでPascal風に書いた処理を、C言語で書いたものである。

unsigned long
check(unsigned long long n)
{
  unsigned long long x=n;
  unsigned long s=0;
  do {
  l1:
    s++;
    if((x&1)!=0) { s++; x=(x>>1)*3+2; goto l1; }
    x>>=1;
  } while(x>=n);
  return s;
}
この関数には、アセンブリ言語に直す途中で見つけた、nとの比較を省略する最適化を適用済みである。また、nとxには、64ビット符号なし整数型を宣言する、unsigned long longを使っている。オーバーフローのチェックはしていない。

これをGCCのバージョン2.7.2p(Pentium対応版)でコンパイルした。出力は特殊なアセンブリ言語で表記されているため、MASMやTASMで使われている表記に直したものを右側につけて示す。

.globl _check					GLOBAL _check
	.type	 _check,@function
_check:					_check:
	pushl %ebp				PUSH EBP
	pushl %edi				PUSH EDI
	pushl %esi				PUSH ESI
	pushl %ebx				PUSH EBX
	movl 20(%esp),%edi			MOV EDI,20[ESP]
	movl 24(%esp),%ebp			MOV EBP,24[ESP]
	movl %edi,%eax				MOV EAX,EDI
	movl %ebp,%edx				MOV EDX,EBP
	xorl %esi,%esi				XOR ESI,ESI
	.align 0,0x90
L13:					L13:
	incl %esi				INC ESI		; U
	testb $1,%al				TEST AL,1	;   V
	je L14					JE L14		; U
	incl %esi				INC ESI		; U
	shrdl $1,%edx,%eax			SHRD EAX,EDX,1	; U   4+1
	shrl $1,%edx				SHR EDX,1	; U
	movl %eax,%ecx				MOV ECX,EAX	;   V
	movl %edx,%ebx				MOV EBX,EDX	; U
	shldl $1,%ecx,%ebx			SHLD EBX,ECX,1	; U   4+1
	sall $1,%ecx				SAL ECX,1	; U
	addl %ecx,%eax				ADD EAX,ECX	; U
	adcl %ebx,%edx				ADC EDX,EBX	; U
	addl $2,%eax				ADD EAX,2	;   V
	adcl $0,%edx				ADC EDX,0	; U
	jmp L13					JMP L13		;   V
	.align 0,0x90
L14:
	shrdl $1,%edx,%eax			SHRD EAX,EDX,1	; U   4+1
	shrl $1,%edx				SHR EDX,1	; U
	cmpl %edx,%ebp				CMP EBP,EDX	; U
	ja L11					JA L11		;   V
	jne L13					JNE L13		; U
	cmpl %eax,%edi				CMP EDI,EAX	; U
	jbe L13					JBE L13		;   V
L11:					L11:
	movl %esi,%eax				MOV EAX,ESI
	popl %ebx				POP EBX
	popl %esi				POP ESI
	popl %edi				POP EDI
	popl %ebp				POP EBP
	ret					RET
ループ中の命令については、命令が実行されるパイプラインと、2クロック以上かかる命令のクロック数をコメントで示してある。+1とあるのは、プリフィックスのための追加クロックである。

分岐予測がすべて成功した場合の、ループ1回のクロック数は、xが偶数で10クロック(JNE L13 が実行されたとき)、xが奇数で18クロックである。初めからアセンブリ言語で書いた場合と比べて、2〜3.6倍のクロック数を要する。遅さの主な原因は、ペアにできずに4クロックを要する、SHLD,SHRD命令を使っていることであるが、それを直してもまだ遅い(それぞれ6クロックと11クロック)。この章で使ったような、アセンブリ言語らしい最適化方法はほとんど使われていない。GCCの最適化は、まだ不十分である。


主な命令一覧

以下は、86系CPUの主な命令と、そのバイト数、実行に必要なクロック数の一覧である。
				Size	Clocks
Opcode		Operands	Bytes	8086	V30	80286	80386	80486	Pentium

NOP				1	3	3	3	3	1	1 UV

MOV		r,r		2	2	2	2	2	1	1 UV
MOV		r,m		2+EA	8+EA	11	5	4	1	1 UV
MOV		m,r		2+EA	9+EA	9	3	2	1	1 UV
MOV		r,i		1+I	4	4	2	2	1	1 UV
MOV		m,i		2+EA+I	10+EA	11	3	2	1	1 UV
MOV		acc,m		3/5	10	10	5	4	1	1 UV
MOV		m,acc		3/5	10	9	3	2	1	1 UV#a

MOV		r,s		2	2	2	2	2	3	1
MOV		m,s		2+EA	9+EA	10	3	2	3	1
MOV		s,r		2	2	2	2	2	3	2
MOV		s,m		2+EA	8+EA	11	5	5	3	3

XCHG		(E)AX,r		1	3	3	3	3	3	2
XCHG		r,r		2	4	3	3	3	3	3
XCHG		m,r		2+EA	17+EA	16	5	5	5#b	3#b

XLAT				1	11	9	5	5	4	4

PUSH		r		1	11	8	3	2	1	1 UV
PUSH		i		1+I		7/8	3	2	1	1 UV
POP		r		1	8	8	5	4	1	1 UV
PUSH		m		2+EA	16+EA	18	5	5	4	2
POP		m		2+EA	17+EA	17	5	5	6	3
PUSH		s		1/2	10	8	3	2	3	1
POP		s		1/2	8	8	5	7	3	3
PUSHF				1	10	8	3	4	4	4
POPF				1	8	8	5	5	9	6
PUSHA				1		35	17	18	11	5
POPA				1		43	19	24	9	5

LAHF				1	4	2	2	2	3	2
SAHF				1	4	3	2	3	2	2

MOVZX MOVSX	r,r		3				3	3	3
MOVZX MOVSX	r,m		3+EA				6	3	3

BSWAP		r		2					1	1

LEA		r,m		2+EA	2+EA	4	3	2	1	1 UV

LDS LES		r,m		2+EA	16+EA	18	7	7	6	4
LFS LGS LSS	r,m		3+EA				7	6	4

ADD SUB AND OR XOR r,r		2	3	2	2	2	1	1 UV
ADD SUB AND OR XOR r,m		2+EA	9+EA	11	7	6	2	2 UV
ADD SUB AND OR XOR m,r		2+EA	16+EA	16	7	7	3	3 UV
ADD SUB AND OR XOR acc,i	1+I	4	4	3	2	1	1 UV
ADD SUB AND OR XOR r,i		2+I	4	4	3	2	1	1 UV
ADD SUB AND OR XOR m,i		2+EA+I	17+EA	18	7	7	3	3 UV

ADC SBB		r,r		2	3	2	2	2	1	1 U
ADC SBB		r,m		2+EA	9+EA	11	7	6	2	2 U
ADC SBB		m,r		2+EA	16+EA	16	7	7	3	3 U
ADC SBB		acc,i		1+I	4	4	3	2	1	1 U
ADC SBB		r,i		2+I	4	4	3	2	1	1 U
ADC SBB		m,i		2+EA+I	17+EA	18	7	7	3	3 U

CMP		r,r		2	3	2	2	2	1	1 UV
CMP		r,m		2+EA	9+EA	11	6	6	2	2 UV
CMP		m,r		2+EA	9+EA	11	7	5	2	2 UV
CMP		acc,i		1+I	4	4	3	2	1	1 UV
CMP		r,i		2+I	4	4	3	2	1	1 UV
CMP		m,i		2+EA+I	10+EA	13	6	5	2	2 UV

TEST		r,r		2	3	2	2	2	1	1 UV
TEST		m,r		2+EA	9+EA	10	6	5	2	2 UV
TEST		acc,i		1+I	4	4	3	2	1	1 UV
TEST		r,i		2+I	5	4	3	2	1	1
TEST		m,i		2+EA+I	11+EA	11	6	5	2	2

INC DEC		r16/32		1	3	2	2	2	1	1 UV
INC DEC		r		2	3	2	2	2	1	1 UV
INC DEC		m		2+EA	15+EA	16	7	6	3	3 UV

NEG NOT		r		2	3	2	2	2	1	1
NEG NOT		m		2+EA	16+EA	16	7	6	3	3

MUL		r8		2	70-77	21-22	13	9-14	13-18#c	11
MUL		r16		2	118-133	29-30	21	9-22	13-26#c	11
MUL		r32		2				9-38	13-42#c	9
MUL		m8		2+EA   76-83+EA	27-28	16	12-17	13-18#c	11
MUL		m16		2+EA 124-139+EA	35-36	24	12-25	13-26#c	11
MUL		m32		2+EA				12-41	13-42#c	9

IMUL		r8		2	80-98	33-39	13	9-14	13-18#c	11
IMUL		r16		2	128-154	41-47	21	9-22	13-26#c	11
IMUL		r32		2				9-38	13-42#c	9
IMUL		m8		2+EA  86-104+EA	39-45	16	12-17	13-18#c	11
IMUL		m16		2+EA 134-160+EA	47-53	24	12-25	13-26#c	11
IMUL		m32		2+EA				12-41	13-42#c	9

IMUL		r16,r16		3				9-22	13-26#c	9
IMUL		r32,r32		3				9-38	13-42#c	9
IMUL		r16,m16		3+EA				12-25	13-26#c	9
IMUL		r32,m32		3+EA				12-41	13-42#c	9
IMUL		r16,r16,i	2+I	    28-34/36-42	21	9-22	13-26#c	9
IMUL		r32,r32,i	2+I				9-38	13-42#c	9
IMUL		r16,m16,i	2+EA+I	    34-40/42-48	24	12-25	13-26#c	9
IMUL		r32,m32,i	2+EA+I				12-41	13-42#c	9

DIV		r8		2	80-90	19	14	14	16	17
DIV		r16		2	144-162	25	22	22	24	25
DIV		r32		2				38	40	41
DIV		m8		2+EA   86-96+EA	25	17	17	16	17
DIV		m16		2+EA 150-168+EA	31	25	25	24	25
DIV		m32		2+EA				41	40	41

IDIV		r8		2	101-112	29-34	17	19	19	22
IDIV		r16		2	165-184	38-43	25	27	27	30
IDIV		r32		2				43	43	46
IDIV		m8		2+EA 107-118+EA	35-40	20	22	20	22
IDIV		m16		2+EA 171-190+EA	44-49	28	30	28	30
IDIV		m32		2+EA				46	44	46

AAA AAS				1	8	3?	3	4	3	3
AAM				2	83	15	16	17	15	10
AAD				2	60	7?	14	19	14	18
DAA DAS				1	4	3?	3	4	2	3

CBW CWDE			1	2	2	2	3	3	3
CWD CDQ				1	5	4-5	2	2	3	2

ROL ROR		r,1		2	2	2	2	3	3	1 U
ROL ROR		m,1		2+EA	15+EA	16	7	7	4	3 U
ROL ROR		r,i		3		7+n	5+n	3	2	1
ROL ROR		m,i		3+EA		19+n	8+n	7	4	3
ROL ROR		r,CL		2	8+4n	7+n	5+n	3	3	4
ROL ROR		m,CL		2+EA   20+4n+EA	19+n	8+n	7	4	5

RCL RCR		r,1		2	2	2	2	9	3	1 U
RCL RCR		m,1		2+EA	15+EA	16	7	10	4	3 U
RCL RCR		r,i		3		7+n	5+n	9	8-30	8
RCL RCR		m,i		3+EA		19+n	8+n	10	9-31	10
RCL RCR		r,CL		2	8+4n	7+n	5+n	9	8-30	7
RCL RCR		m,CL		2+EA   20+4n+EA	19+n	8+n	10	9-31	9

SHL SHR SAL SAR	r,1		2	2	2	2	3	3	1 U
SHL SHR SAL SAR	m,1		2+EA	15+EA	16	7	7	4	3 U
SHL SHR SAL SAR	r,i		3		7+n	5+n	3	2	1 U
SHL SHR SAL SAR	m,i		3+EA		19+n	8+n	7	4	3 U
SHL SHR SAL SAR	r,CL		2	8+4n	7+n	5+n	3	3	4
SHL SHR SAL SAR	m,CL		2+EA   20+4n+EA	19+n	8+n	7	4	5

SHLD SHRD	r,r,i		4				3	2	4
SHLD SHRD	m,r,i		4+EA				7	3	5
SHLD SHRD	r,r,CL		3				3	3	4
SHLD SHRD	m,r,CL		3+EA				7	4	5

BT		r,r		3				3	3	4
BT		m,r		3+EA				12	8	9
BT		r,i		4				3	3	4
BT		m,i		4+EA				6	3	4

BTR BTS BTC	r,r		3				6	6	7
BTR BTS BTC	m,r		3+EA				13	13	14
BTR BTS BTC	r,i		4				6	6	7
BTR BTS BTC	m,i		4+EA				8	8	8

SETcc		r		3				4	4/3	1
SETcc		m		3+EA				5	3/4	2

Jcc		short/near	2/4/6	16/4	14/4	7+m/3	7+m/3	3/1	1 V
JMP		short/near	2/3/5	15	12/13	7+m	7+m	3	1 V
JMP		far		5/7	15	15	11+m	12+m	17	3
JMP		r		2	11	11	7+m	7+m	5	2
JMP		m		2+EA	18+EA	20	11+m	10+m	5	2
JMP		m(far)		2+EA	24+EA	27	15+m	17+m#d	13	4

CALL		near		3/5	19	16	7+m	7+m	3	1 V
CALL		far		5/7	28	21	13+m	17+m	18	4
CALL		r		2	16	14	7+m	7+m	5	2
CALL		m		2+EA	21+EA	23	11+m	10+m	5	2
CALL		m(far)		2+EA	37+EA	31	16+m	22+m	17	5

RETN				1	16	15	11+m	10+m	5	2
RETN		i16		3	20	20	11+m	10+m	5	3
RETF				1	26	21	15+m	18+m	13	4
RETF		i16		3	25	24	15+m	18+m	14	5

JCXZ JECXZ	short		2	18/6	13/5	8+m/4	9+m/5	8/5	6/5
LOOP		short		2	17/5	13/5	8+m/4	11+m	7/6	5/6
LOOPZ		short		2	18/6	14/5	8+m/4	11+m	9/6	7/8
LOOPNZ		short		2	19/5	14/5	8+m/4	11+m	9/6	7/8

BOUND		r,m		2+EA		18	13	10	7	8
ENTER		i16,0		4		16	11	10	14	11
ENTER		i16,1		4		19	15	12	17	17
ENTER		i16,i8		4		11+8n	12+4n	11+4n	17+3n	15+2n
LEAVE				1		6	5	4	5	3

CLC STC CMC CLD STD		1	2	2	2	2	2	2
CLI				1	2	2	3	8	5	6
STI				1	2	2	2	8	5	7

LODS				1	12	7	5	5	5	2
REP LODS			2	9+13n	7+9n	5+4n	5+6n	7+4n/5	7+3n
STOS				1	11	7	3	5	5	3
REP STOS			2	9+10n	7+4n	4+3n	5+5n	7+4n/5	10+n/7
MOVS				1	18	11	5	8	7	4
REP MOVS			2	9+17n	11+8n	5+4n	8+4n	13+3n/5	12+n/6
SCAS				1	15	7	7	8	6	4
REP(N)E SCAS			2	9+15n	7+10n	5+8n	5+8n	7+5n/5	9+4n/7
CMPS				1	22	13	8	10	8	5
REP(N)E CMPS			2	9+22n	7+14n	5+9n	5+9n	7+7n/5	8+4n/7

#a ペアリングの際、accに書き込むかのように扱われる。
#b バスロックなどのため実際にはもっとかかる。Pentiumで20クロック以上。
#c DX4では8ビットが5、16ビットが5-6、32ビットが6-12。
#d 43+mとなっている文献もある。
		Size	Clocks
Opcode Operands	Bytes	87/287	287XL	387	486/487	Pentium

FLD	ST(i)	2	17-22	21	7-12	4	1 X
FLD	m32	2+EA   38-56+EA	36	9-18	3	1 X
FLD	m64	2+EA   40-60+EA	45	16-23	3	1 X
FLD	m80	2+EA   53-65+EA	48	12-43	6	3
FBLD	m80	2+EA 290-310+EA	270-279	45-97	70-103	48-58
FST	ST(i)	2	15-22	18	7-11	3	1
FSTP	ST(i)	2	17-24	19	7-11	3	1
FST(P)	m32	2+EA   84-90+EA	51	25-43	7*a	2*c
FST(P)	m64	2+EA  96-104+EA	56	32-44	8*b	2*c
FSTP	m80	2+EA   52-58+EA	61	46-52	6	3*c
FBSTP	m80	2+EA 520-540+EA	520-542	112-190	172-176	148-154
FILD	m16	2+EA   46-54+EA	61-65	42-53	13-16	3(2/2)
FILD	m32	2+EA   52-60+EA	61-68	26-42	9-12	3(2/2)
FILD	m64	2+EA   60-68+EA	76-87	26-54	10-18	?
FIST	m16	2+EA   80-90+EA	88-101	58-76	29-34	6
FISTP	m16	2+EA   82-92+EA	88-101	58-76	29-34	6
FIST	m32	2+EA   82-92+EA	86-100	57-76	28-34	6
FISTP	m32	2+EA   84-94+EA	86-100	57-76	28-34	6
FISTP	m64	2+EA  94-105+EA	91-108	60-82	29-34	?
FLDZ		2	11-17	27	10-17	4	2
FLD1		2	15-21	31	15-22	4	2
FLDL2E		2	15-21	47	26-36	8	5
FLDL2T		2	16-22	47	26-36	8	5
FLDPI		2	16-22	47	26-36	8	5
FLDLN2		2	17-23	48	26-38	8	5
FLDLG2		2	18-24	48	25-35	8	5
FNSTSW	AX	2	10-16	18	13	3	6
FNSTSW	m16	2+EA   12-18+EA	18	15	3	6
FLDCW	m16	2+EA	7-14+EA	33	19	4	8
FNSTCW	m16	2+EA   12-18+EA	18	15	3	2

F2XM1		2	310-630	215-483	167-410	140-179
FABS		2	10-17	29	14-21	3
FADD  ST,ST(i)	2	70-100	30-38	12-16	8-20
FADD  ST(i),ST	2	70-100	33-41	15-29	8-20
FADDP ST(i),ST	2	75-105	33-41	15-29	8-20
FADD	m32	2+EA  90-120+EA	40-48	12-29	8-20
FADD	m64	2+EA  95-125+EA	49-79	15-34	8-20
FCHS		2	10-17	31-37	17-24	6
FNCLEX		2	2-8	8	11	7
FCOM	ST(i)	2	40-50	31	13-21	4
FCOMP	ST(i)	2	42-52	33	13-21	4
FCOMPP		2	45-55	33	13-21	5
FCOM	m32	2+EA   60-70+EA	42	13-25	4
FCOM	m64	2+EA   65-75+EA	51	14-27	4
FCOMP	m32	2+EA   63-73+EA	42	13-25	4
FCOMP	m64	2+EA   67-77+EA	51	14-27	4
FCOS		2		130-779	122-680	193-279
FDECSTP		2	6-12	29	22	3
FNDISI		2	2-8
FDIV   ST,ST(i)	2	193-203	95	77-80	73
FDIVR  ST,ST(i)	2	194-204	95	77-80	73
FDIV   ST(i),ST	2	193-203	98?	80-83?	73
FDIVR  ST(i),ST	2	194-204	95?	77-80?	73
FDIVP  ST(i),ST	2	197-207	98	80-83	73
FDIVRP ST(i),ST	2	198-208	98	80-83	73
FDIV	m32	2+EA 215-225+EA	105	77-85	73
FDIVR	m32	2+EA 216-226+EA	105	77-85	73
FDIV	m64	2+EA 220-230+EA	114	88?-91	73
FDIV	m64	2+EA 221-231+EA	114	81-91	73
FNENI		2	2-8
FFREE	ST(i)	2	9-16	25	18	3
FFREEP	ST(i)	2	13-21	25	18	3
FIADD	m16	2+EA 102-137+EA	71-85	38-64	20-35
FIADD	m32	2+EA 108-143+EA	73-78	34-56	19-32
FICOM	m16	2+EA   72-86+EA	71-75	39-62	16-20
FICOM	m32	2+EA   78-91+EA	72-79	34-52	15-17
FICOMP	m16	2+EA   74-88+EA	71-77	39-62	16-20
FICOMP	m32	2+EA   80-93+EA	72-79	34-52	15-17
FIDIV	m16	2+EA 224-238+EA	136-140	105-124	85-89
FIDIV	m32	2+EA 230-243+EA	136-143	101-104	84-86
FIDIVR	m16	2+EA 225-239+EA	135-141	135-141	85-89
FIDIVR	m32	2+EA 231-245+EA	137-144	102-115	84-86
FIMUL	m16	2+EA 124-138+EA	76-87	46-74	23-27
FIMUL	m32	2+EA 130-144+EA	77-88	43-71	22-24
FINCSTP		2	6-12	28	21	3
FNINIT		2	2-8	25	33	17
FISUB	m16	2+EA 102-137+EA	71-83	38-64	20-35
FISUB	m32	2+EA 108-143+EA	73-98	34-56	19-32
FISUBR	m16	2+EA 103-139+EA	72-84	39-65	20-35
FISUBR	m32	2+EA 109-144+EA	74-99	35-57	19-32
FMUL   ST,ST(i)	2	130-145	42-50	46-54	16
FMUL   ST(i),ST	2	130-145	25-53	17-50	16
FMULP  ST(i),ST	2	134-148	25-53	17-50	16

*a 値が0.0のときは27。
*b 値が0.0のときは28。
*c 値は1クロック前に必要。

同一クロックスピードで比べたときの、各CPUの実行速度の目安は、次の通りである。

8086→80286: 約3倍
80286→80386: ほぼ同じ
80386→80486: 約2倍
80486→Pentium: 2倍弱
Pentium→PentiumPro/II/III: 場合による
PentiumPro/II/III→Pentium4: 約0.8倍

参考文献

  1. Robert L. Hummel 著 槌田浩一 訳, 80x86/80x87ファミリー・テクニカルハンドブック, 技術評論社, 1993.
  2. Torbjörn Granlund and Peter L. Montgomery, Division by Invariant Integers using Multiplication, Proceedings of the SIGPLAN '94 Conference on Programming Language Design and Implementation, PP. 61-72, 1994. (http://www.swox.com/~tege/)
  3. 菅原清文, MMXテクノロジオフィシャルガイド, ソフトバンク, 1997.
  4. Agner Fog, How to optimize for the Pentium family of microprocessors, http://www.agner.org/assem/, 1996-2004. 日本語訳
  5. 藤波順久, 整数定数による除算のための最良の命令列生成法, 平成16年度 夏のプログラミング・シンポジウム報告集, 2004. (論文などに掲載)