アセンブラでの高速化

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

アルゴリズムを工夫して計算量を減らす。

無駄な計算をしない。

何度も使う値は先に計算しておく。

実行に時間のかかっている部分を見つけてそこを最適化する。

内側のループに注目して最適化する。

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

この文章では、レジスタを次のように表記する。文中であっても「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

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

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

CMP BX,0

TEST BX,BX

Bytes 8086 80286 80386 80486 CMP BX,0 3 4 3 2 1 TEST BX,BX 2 3 2 2 1

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

0に関する操作

BXを0にする(フラグが保存されなくてよい場合) Bytes 8086 80286 80386 80486 MOV BX,0 3 4 2 2 1 XOR BX,BX 2 3 2 2 1 XORの代わりにSUBでも同様

BXと0を比較する(AFは不要の場合) Bytes 8086 80286 80386 80486 CMP BX,0 3 4 3 2 1 TEST BX,BX 2 3 2 2 1 TESTの代わりにANDまたはORでも同様

メモリ上のワードデータを0にする(AXとフラグが保存されなくてよい場合) Bytes 8086 80286 80386 80486 MOV [ADDR],0 6 16 3 2 2# XOR AX,AX / MOV [ADDR],AX 5 13 5 4 2 # オフセットと即値の追加クロックを含む 続けていくつものデータを0にする場合は、かなりバイト数の節約になる。なお、連続したデータ領域を0で埋めるには、REP STOS命令を使うとよい。

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

CMP [ADDR],0

INC,DEC命令

例 ;SIが指す多倍長数を、DIが指す多倍長数に加える ;多倍長数の長さはCXワード CLC L1: LODSW ADC [DI],AX INC DI INC DI LOOP L1 JC L2 ; オーバーフローしたときの処理

ワードレジスタに対する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

アキュームレータ

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クロック短縮)

即値

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

例 AND BX,0FFFEH → AND BL,0FEH (フラグは異なる) OR BX,8000H → OR BH,80H (フラグは異なる) TEST BX,0006H → TEST BL,06H

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

アドレシングモード

[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ビットの変位(定数)

オフセットBX+4のメモリを読んでAXに入れる Bytes 8086 80286 80386 80486 ADD BX,4 / MOV AX,[BX] 5 17 8 6 4# MOV AX,[BX+4] 3 17 5 4 1 # アドレス生成インターロックのペナルティーを含む BXの値を変化させたくない場合は特に、後者が有利である。

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

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

LEA命令

BXと即値の加算結果をCXに入れる Bytes 8086 80286 80386 80486 MOV CX,BX / ADD CX,8 5 6 5 4 2 LEA CX,[BX+8] 3 11 3 2 1 8086では遅くなるので注意する。使えるレジスタの組合せに制限があるが、[BX+SI]のようなレジスタ同士の加算や、[BX+SI+8]のような3オペランドの加算もできる。

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

オフセットアドレスをBXに入れる Bytes 8086 80286 80386 80486 LEA BX,[ADDR] 4 8 3 2 1 MOV BX,OFFSET ADDR 3 4 2 2 1 このコードをアセンブリ言語で書く場合、次の点に注意する。 ADDRがGROUP擬似命令でグループ化されたセグメント内のシンボルの場合、 MOV BX,OFFSET DGROUP:ADDR のように、グループ名を明記する必要がある。ASSUME擬似命令でセグメントレジスタがそのグループを指していることを宣言している場合、そのセグメントレジスタを使って、 MOV BX,OFFSET DS:ADDR のように書いてもよい。 アセンブラによっては、LEA命令を使って書いても、自動的にMOV命令を使ったコードを出力してくれる(TASM 2.0など)。その場合には、上記の問題も同時に解決するので便利である。

このコードをアセンブリ言語で書く場合、次の点に注意する。

AXを左に3回ローテート Bytes 8086 80286 80386 80486 MOV CL,3 / ROL AX,CL 4 24 10 5 4 ROL AX,1 / ROL AX,1 / ROL AX,1 6 6 6 9 9 CLの値を変えたくないときには、後者が2バイト長い欠点は相殺される。

AXを左または右に8回ローテート(フラグは不要の場合) Bytes 8086 80286 80386 80486 ROL AX,8 3 13 3 3 ROR AX,8 3 13 3 3 XCHG AH,AL 2 3 3 3 3

AXを左に8回シフト(フラグは不要の場合) Bytes 8086 80286 80386 80486 SHL AX,8 3 13 3 3 MOV AH,AL / XOR AL,AL 4 5 4 4 3# # 部分レジスタストールのペナルティーを含む

AXを右に8回シフト(符号なし)(フラグは不要の場合) Bytes 8086 80286 80386 80486 SHR AX,8 3 13 3 3 MOV AL,AH / XOR AH,AH 4 5 4 4 3# # 部分レジスタストールのペナルティーを含む

AXを右に8回シフト(符号つき)(フラグは不要の場合) Bytes 8086 80286 80386 80486 SAR AX,8 3 13 3 3 MOV AL,AH / CBW 3 4 4 5 4

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

AXを2倍にする(AFは不要の場合) Bytes 8086 80286 80386 80486 ADD AX,AX 2 3 2 2 1 SHL AX,1 2 2 2 3 3

AXをキャリーつきで2倍にする(PF,AF,ZF,SFは不要の場合) Bytes 8086 80286 80386 80486 ADC AX,AX 2 3 2 2 1 RCL AX,1 2 2 2 9 3

オペランドの値が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:

MOV AX,100 TEST BP,BP JZ L2 MOV AX,200 L2:

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

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

DB 0A8H ; TEST AL,n

DB 0A9H ; TEST AX,nn

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

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

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

JAE L3

JNAE L99 JMP L3 L99:

JAE TO_L3

TO_L3: JMP L3

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

コール/リターン

CALL SUB1 RET

JMP SUB1

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

JNAE L99 RET L99:

JAE L_RET

L_RET: RET

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

SUB1: CMP SI,[BUFFER_END] JNE L1 ; ; 長い処理 ; L1: LODSB RET

LOOP L1 CALL SUB2 L1: LODSB

SUB2: ; ; 長い処理 ; MOV CX,[BUFFER_END] SUB CX,SI RET

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

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

呼び出し元と先が同じセグメントのfarルーチンを呼ぶ Bytes 8086 80286 80386 80486 CALL FAR PTR SUB2 6 28 13+m 17+m 18 PUSH CS / CALL NEAR PTR SUB2 5 29 10+m 9+m 6 この最適化を自動的に行うアセンブラもある(TASM 2.0など)。8086では1クロック損するが、元が遅いのであまり気にしなくてよい。命令フェッチまで考えると、すぐにPUSH CSを実行できるぶん速いかもしれない。

レジスタ

オフセットBXのメモリのワードデータに3を加える。 Bytes 8086 80286 80386 80486 MOV AX,[BX] / ADD AX,3 / MOV [BX],AX 7 31 11 8 3 ADD WORD PTR [BX],3 3 21 7 7 3

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以降のプロテクトモードでは、セグメントレジスタに好きな値を入れることはできない。

スタック

MOV CX,10 L1: PUSH CX ; ; いろいろな処理 ; POP CX LOOP L1

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

PUSH AX PUSH BP MOV BP,SP MOV [BP-2],999 POP BP

サブルーチンの入口で、使うレジスタを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

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

なお、

PUSH BX RET

PUSH ES PUSH BX RETF

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

CALL L3 L3: POP BX

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

CALL SUB RET

JMP SUB

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

演算結果が0か、正か、負か、符号つき/符号なしのオーバーフローなどを判断する。

比較の結果が等しいか、符号つき/符号なしで大きいか/小さいかなどを判断する。

多倍長演算(ADC,SBB命令では、CFをキャリーまたはボローとして使う)

ローテート/シフト命令であふれたビットがCFにはいる。RCL,RCR命令は、CF経由でローテートする。多倍長のシフトや、ビット順反転に利用できる。

10進補正命令は、前の演算で設定されたAFの値を使う。

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

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:

メモリ

A DB ? B DB ?

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以降ではキャッシュを有効に使うことになる。ただし、誤って同時に使ってしまわないように、常に気をつける必要がある。

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

セグメント

もし、速度的に重要な場所でセグメントの大きさを越えるデータを使うときには、セグメント内の処理を行うループの外に、セグメントレジスタを設定するコードを置くようにし、セグメントレジスタを頻繁にいじるのは避けるべきである。例えば、セグメントアドレス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の指すセグメントに格納することができる。こうすれば、セグメントレジスタの値を変えずに全データをアクセスできる。

値の利用

XOR AX,AX MOV [X],AX L1: ; ; 処理 ; MOV AX,[X] INC AX MOV [X],AX CMP AX,[X_LIMIT] JNE L1

複数のレジスタに同じ値を入れたいときには、一つのレジスタに入れた値を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

アラインメント

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

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

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

MOV CX,AX SHL AX,1 ADD AX,CX

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

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命令

特定のレジスタを必要とする命令を、値を変えて交互に使う。例えば、DX:AXから始まるCX個の32ビット数を、ES:[DI]から順にストアするには、 L1: STOSW XCHG AX,DX STOSW XCHG AX,DX ADD AX,1 ADC DX,0 LOOP L1 とする。サブルーチン、ファンクションリクエストを呼ぶのにも使える。余分なレジスタを使う必要がないのが利点である。

とする。サブルーチン、ファンクションリクエストを呼ぶのにも使える。余分なレジスタを使う必要がないのが利点である。 レジスタの退避と値のロードを同時に行う。同様に、値のストアとレジスタの復帰を同時に行う。例えば、 PUSH BX MOV BX,[POINTER] ; ; 処理 ; MOV [POINTER],BX POP BX の代わりに XCHG BX,[POINTER] ; ; 処理 ; XCHG BX,[POINTER] とする。 Bytes 8086 80286 80386 80486 PUSH BX / MOV BX,[POINTER] 5 24 8 6 2 XCHG BX,[POINTER] 4 22 5 5 5 Bytes 8086 80286 80386 80486 MOV [POINTER],BX / POP BX 5 22 8 6 2 XCHG BX,[POINTER] 4 22 5 5 5

の代わりに とする。 2回のメモリアクセスを1命令にまとめる。例えば、メモリ上のワードデータA,B,Cを、A→B→Cとコピーしたいとき、 MOV AX,[B] MOV [C],AX MOV AX,[A] MOV [B],AX とする代わりに、 MOV AX,[A] XCHG AX,[B] MOV [C],AX とする。 Bytes 8086 80286 80386 80486 MOV AX,[B] / MOV [C],AX / … 12 40 16 12 4 MOV AX,[A] / XCHG AX,[B] / MOV [C],AX 10 43 13 11 7 メモリ上のワードデータをAXに入れ、BXをそこに書き込む場合は次のようになる。 Bytes 8086 80286 80386 80486 MOV AX,[V] / MOV [V],BX 7 25 8 6 2 MOV AX,BX / XCHG [V],AX 6 25 7 7 6 どちらも、8086では速くならないが、AXが他のレジスタだと速くなる。

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

BXをAXにコピーする(BXが変化してよい場合) Bytes 8086 80286 80386 80486 MOV AX,BX 2 2 2 2 1 XCHG AX,BX 1 3 3 3 3

AAM,AAD命令

応用

ビットはめ込み ビットマップの操作などで、用意したマスクのビットが1の桁にだけ、あるパターンをはめ込みたいことがある。例えば、 元のビットマップ 01110111 パターン 10101010 マスク 00111100 結果 01101011 のような処理である。元のビットマップが[DI]に、パターンがAXに、マスクがDXにある場合、 AND AX,DX NOT DX AND [DI],DX OR [DI],AX のように処理すればできるが、 XOR AX,[DI] AND AX,DX XOR [DI],AX のようにしたほうが速く短い。

のような処理である。元のビットマップが[DI]に、パターンがAXに、マスクがDXにある場合、 のように処理すればできるが、 のようにしたほうが速く短い。 マスクの生成 上記のようにしてポリゴンなどを描画するには、与えられた範囲のマスクを生成する必要がある。 下位のCL桁が0で残りは1 MOV AX,0FFFFH SHL AX,CL 下位のCL桁が1で残りは0 MOV AX,0001H SHL AX,CL DEC AX 下位のCL+1桁が0で残りは1 MOV AX,0FFFEH SHL AX,CL 下位のCL+1桁が1で残りは0 MOV AX,0002H SHL AX,CL DEC AX 上位のCL桁が0で残りは1 MOV AX,0FFFFH SHR AX,CL 上位のCL桁が1で残りは0 MOV AX,0FFFFH SHR AX,CL NOT AX 上位のCL+1桁が0で残りは1 MOV AX,7FFFH SHR AX,CL 上位のCL+1桁が1で残りは0 MOV AX,8000H SAR AX,CL PC-9800シリーズのグラフィックVRAMのように、バイト毎にmsbが左端に対応しているような場合には、バイト単位でマスクを作るか、ワード単位のマスクを作ってから、上下のバイトを入れ替えればよい。

下位のCL桁が1で残りは0 下位のCL+1桁が0で残りは1 下位のCL+1桁が1で残りは0 上位のCL桁が0で残りは1 上位のCL桁が1で残りは0 上位のCL+1桁が0で残りは1 上位のCL+1桁が1で残りは0 ビット交換 例えば、ALのビット2と4を入れ替えるには、次のようにするとよい。 TEST AL,14H JPE SKIP XOR AL,14H SKIP: PFは下位8ビットについての結果を反映するため、この方法は8ビットレジスタにしか使えない。 例えば、AXのビット2-4と7-9を入れ替えるには、次のようにする。 MOV CX,AX SHR CX,5 XOR CX,AX AND CX,1CH XOR AX,CX SHL CX,5 XOR AX,CX

PFは下位8ビットについての結果を反映するため、この方法は8ビットレジスタにしか使えない。 ビット逆順 ALのビットの順番を逆転させるには、次のようにする。 MOV CL,AL AND AL,55H XOR CL,AL ROL AL,2 OR AL,CL MOV CL,AL AND AL,66H XOR CL,AL ROL AL,4 OR AL,CL ROL AL,1 AXなら次のようにする。80486以降では、最後から3番目の XCHG AH,AL は ROL AX,8 のほうがよい。 MOV CX,AX AND AX,5555H XOR CX,AX ROL AX,2 OR AX,CX MOV CX,AX AND AX,6666H XOR CX,AX ROL AX,4 OR AX,CX MOV CX,AX AND AX,7878H XOR CX,AX XCHG AH,AL OR AX,CX ROL AX,1

16進10進変換 CMP AL,10 SBB AL,69H DAS

絶対値 CWD XOR AX,DX SUB AX,DX あるいは MOV DX,AX SAR DX,15 XOR AX,DX SUB AX,DX

あるいは 符号 CWD NEG AX ADC DX,DX あるいは ROL AX,1 SBB DX,DX NEG AX ADC DX,DX

あるいは 小さい方を選ぶ SUB AX,CX SBB DX,DX AND AX,DX ADD AX,CX

漢字コード変換 JIS漢字コードとMS漢字コード(シフトJIS)との変換を行う。86系で自然な、漢字コードの1バイト目をAL、2バイト目をAHに入れる場合を仮定する。 JIS漢字コードからMS漢字コードへ ADD AX,(9Fh-21h)*256+(42h-21h) SHR AL,1 JC SKIP CMP AH,60h+(9Fh-21h) SBB AH,9Fh-40h-1 SKIP: XOR AL,0A0h MS漢字コードからJIS漢字コードへ AND AL,3Fh SHL AL,1 SUB AH,9FH JAE SKIP DEC AX CMP AH,80h-9Fh ADC AH,9Fh-40h-1 SKIP: ADD AX,2120h



パック演算

MOV AX,SI XOR AX,DI MOV DX,SI AND DX,DI

ループ

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

DEC SI L3: INC SI CMP BYTE PTR [SI],0 JNZ L3

DB 0A8H ; TEST AL,n L3: INC SI CMP BYTE PTR [SI],0 JNZ L3

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

CMP BYTE PTR [SI],0 JNZ 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の計算 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つの値を交代で使いたいときは、次のようにする。

3つの定数a,b,c ループ外: MOV AX,a MOV CX,a XOR b ループ中: XOR AX,CX XOR CX,AX XOR CX,a XOR b XOR c

3つのレジスタの値AX,CX,DX ループ外: XOR CX,AX XOR DX,CX ループ中: XOR AX,CX XOR CX,AX XOR CX,DX

4つの定数a,b,c,d ループ外: MOV AX,a MOV CX,a XOR b MOV DX,a XOR c ループ中: XOR AX,CX XOR CX,DX XOR DX,a XOR b XOR c XOR d

上で a XOR b XOR c XOR d が0のとき ループ外: MOV AX,a MOV CX,a XOR b ループ中: XOR AX,CX XOR CX,a XOR c ; XOR DX,b XOR d と同じ

4つのレジスタの値AX,CX,DX,BX ループ外: XOR CX,AX XOR BX,CX XOR BX,DX XOR DX,AX ループ中: XOR AX,CX XOR CX,DX XOR DX,BX

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

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

LODSB INC AL STOSB LODSB INC AL STOSB

LODSW INC AL INC AH STOSW

最初の境界までのビットの処理 バイトまたはワードごとの処理 残ったビットの処理

ループではないが、再帰呼び出しされるサブルーチンで、引数が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の種類を判別するには、次のようにする。

準備中

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

32ビット命令

特に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ビットの変位(定数)

インデックスレジスタを使うと、使わないときと比べて命令が1バイト長くなる。

インデックスレジスタを使わなくても、ESPをベースレジスタにした場合には、他のレジスタと比べて命令が1バイト長くなる。

ESPをインデックスレジスタにすることはできない。

変位は8または32ビットなので、変位が-128〜+127の範囲を出ると命令が3バイト長くなる。

[index*scale]という形式はないので、これを使おうとすると、[offset+index*scale]の形式がoffsetが0として使われ、[base+index*scale]の形式より4バイト長くなる。

16ビットアドレシングモードと同様に、EBPをベースレジスタとして使う、変位なしの形式はない。

ベースレジスタがESPまたはEBPのときは、デフォルトセグメントがSSになる。通常はDSである。

80386 ベースレジスタとインデックスレジスタの両方を使うと、1クロック追加。 80486 インデックスレジスタを使うと、1クロック追加。即値を含む命令で、変位またはオフセットを使うと1クロック追加。 Pentium 即値を含む命令で、変位またはオフセットを使うと、ペアリング不可。 PentiumPro なし

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

PUSH CX PUSH DX POP ECX

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

DXのビットCXをテストする。 Bytes 80386 80486 MOV AX,1 / SHL AX,CL / TEST DX,AX 7 7 5 BT DX,CX 3 3 3

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

DXのビットCXをセットする。 Bytes 80386 80486 MOV AX,1 / SHL AX,CL / OR DX,AX 7 7 5 BTS DX,CX 3 6 6

レジスタに対するビット操作命令と、ビット位置が即値のビット操作命令では、ビット位置は下位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

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

LEA命令

例: 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倍

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

EAXに3を入れる(16ビットモード) Bytes 80386 80486 MOV EAX,3 6 2 2# LEA EAX,DS:[3] 5 2 2# # サイズプリフィックスのデコードのための追加クロックを含む

EBXに16ビットセグメントのシンボルVのオフセットを入れる(16ビットモード) Bytes 80386 80486 MOV EBX,OFFSET V 6 2 2# LEA EBX,[V] 5 2 2# # サイズプリフィックスのデコードのための追加クロックを含む

ローテート/シフト

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

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バイト境界をまたがないようにする。

; 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 ; 次のコード

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

リアルモード用の開発ツール(リンカ、デバッガ)を利用できる。

他の、リアルモード用のプログラム(デバイスドライバなど)と組み合わせるときに、モード切り替えのオーバーヘッドがない。

16ビットモードで動作するため、32ビット命令や32ビットアドレシングを使うと、プリフィックスが必要になり、コードが長くなる。80486以降では、実行速度も低下する。

仮想86モードでは、この状態を利用できない。

BSWAP命令

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

遅い命令

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

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

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

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

オフセットEBXのバイトデータをゼロ拡張してEAXに入れる(32ビットモード) Bytes 80386 80486 MOVZX EAX,BYTE PTR [EBX] 3 3 3 XOR EAX,EAX / MOV AL,[EBX] 4 6 2

シフト命令は高速化されなかったので、なるべく使わないほうがよい。ローテート/シフトで説明した、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は、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のどちらが最近使われたかを記録している。キャッシュラインを置き換えるときには、この情報に基づいて古いほう捨てる。そのため、最も古いものではなく二番目に古いものを捨てることがある。

この後、キャッシュラインが満たされる前に、同じキャッシュライン中のデータをアクセスした場合、マニュアル上ではキャッシュライン全体を満たすまで待たされることになっているが、実際にはもっと長く(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]

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

XOR AH,AH INC AX

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を読み出してもストールは発生しない。

ADD EBX,4 MOV EAX,[EBX] INC ECX

ADD EBX,4 INC ECX MOV EAX,[EBX]

MOV EAX,[EBX+4] ADD EBX,4 INC ECX

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命令に即値オペランドがある場合に限り、次の命令でアドレス生成インターロックが発生する。

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

U,Vどちらのパイプでもペアにできる MOV r/m,r/m/i

PUSH r/i

POP r

LEA r,m

ADD SUB AND OR XOR CMP r/m,r/m/i

TEST r/m,r/m

TEST acc,i

INC DEC r/m Uパイプでのみペアにできる ADC SBB r/m,r/m/i

ROL ROR RCL RCR r/m,1

SHL SHR SAL SAR r/m,i Vパイプでのみペアにできる Jcc JMP short/near

CALL near

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

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

先の命令はUパイプでペアにでき、後の命令はVパイプでペアにできる。

先の命令が書き込んだレジスタを、後の命令で読んだり書いたりしない。これにはいくつか例外がある。 PUSHとPUSH、PUSHとCALL、POPとPOPはペアにできる(スタックポインタの特例)。PUSHとPOP、POPとPUSHもペアにできるが、同じメモリをアクセスするため同時には実行されない。 後の命令が条件ジャンプ命令のときは、先の命令がフラグレジスタに書き込んでいてもペアにできる(フラグレジスタの特例)。 両方の命令がフラグレジスタに書き込んでいてもペアにできる(フラグレジスタの特例)。 部分レジスタは全レジスタとして扱われる。 MOV命令のうち、 MOV m,acc の形式は、accに書き込むかのように扱われる。

MMXなしPentiumでは、プリフィックスつきの命令は、Uパイプでのみペアにできる。ただし、near条件ジャンプはVパイプでペアにできる。MMX対応Pentiumでは、サイズプリフィックスがついた命令や0Fhで始まる命令も、両方のパイプでペアにできる。

オフセットと即値、または変位と即値を持つ命令は、MMXなしPentiumではペアにできず、MMX対応PentiumではUパイプでのみペアにできる。

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

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

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

ペアにできない命令の使用を避け、複雑な命令を単純な命令に分割するなどして、ペアにできる命令を使う。ペアにすると後の命令の実行開始が遅れる、read-modify-write命令も、3命令に分割するとよい。

ペアにできるように命令を並べ替える。

キャッシュバンクを意識して命令を並べ替える。例えば、 MOV [EBX],AL MOV [EBX+1],AL MOV [EBX+9],AL MOV [EBX+10],AL は MOV [EBX],AL MOV [EBX+9],AL MOV [EBX+1],AL MOV [EBX+10],AL とする。

は とする。 16ビットコードでPUSHやPOPを続けるときは、初めのスタックポインタの値が4の倍数+2になるようにして、キャッシュバンク競合を避ける。

キャッシュ

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

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

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

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

-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のレジスタの組み合わせに該当する」とあるが、実際にはストールは観察されなかった。

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

分岐予測に成功した命令は、命令一覧に書かれたクロック数で実行できる。

分岐予測に失敗した命令は、通常は3クロック、ペアになって実行される条件ジャンプの場合は、4クロックの追加クロックが必要になる。

分岐予測に失敗した場合、次に実行される命令ペアに分岐命令が含まれていると、これは分岐しないと予測される。分岐する命令の実行が続く場合、最初に予測ミスが起きると、予測ミスがずっと続くことになる。

分岐しないと予測されるのは、Branch Target Bufferにない命令と、続けて2回以上分岐しなかった命令だけである。それ以外はすべて分岐すると予測される。

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

条件ジャンプはなるべくジャンプするように配置する。

予測ミスの起きやすい分岐命令の後に実行される命令ペアに、分岐命令を入れないようにする。

コードの重要な部分では、サブルーチンを異なる場所から交互に呼んだりしないようにする。

追加された命令

FSTSW AX SAHF

キャッシュ

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

MOV AL,[ESI] MOV AH,AL SHL EAX,8 ; ストール MOV AL,AH MOV [EDI],EAX ; ストール

MOVZX EAX,BYTE PTR [ESI] MOV ECX,EAX SHL EAX,16 MOV EDX,ECX SHL ECX,8 OR EAX,EDX OR EAX,ECX

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]

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

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

MOV [EBX],AL MOV ECX,[EBX]

MOV [EBX],EAX MOV ECX,[EBX+2]

MOV [EBX],EAX MOV AL,[EBX+2]

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

INC命令(CFを変更しない)の後の、CFを使う命令(JC,ADCなど) ROL,ROR命令(PF,ZF,SFを変更しない)の後の、PF,ZF,SFを使う命令(JZなど) SAHF命令(FLAGSの下位8ビットのみを変更)の後の、FLAGSの下位8ビットとそれ以外の両方を使う命令(JLなど)

ADD EAX,ECX / ADC EDX,0 ADD EAX,ECX / LEA EBX,[EDX+1] / CMOVC EDX,EBX

トレースキャッシュ

分岐ヒント

ストアフォワーディング

問題

もし偶数なら、2で割る。

もし奇数なら、3倍して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

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が偶数のときは省いてよいなど、計算の手間を減らす方法がいろいろあるが、本題からはずれるので省略する。

アセンブリ言語へ

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

TEST AL,01H JZ Z2

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 ;*

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 ;*

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:

SHR EDX,1 RCR EAX,1 JNC E2 … E2:

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:

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

L23: MOV ESI,1 JMP E3

JNC L2 MOV ESI,1 JMP E3

最後の、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向けの最適化

最初の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

次の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

GCCとの比較

次の関数は、この章の始めで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; }

これを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

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

オペランドの説明 r: レジスタ acc: EAX/AX/AL m: メモリ i: 即値 s: セグメントレジスタ

サイズの説明 I: 即値のバイト数 EA: アドレス指定のバイト数(表参照)

クロック数の説明 EA: 8086の場合の追加クロック数(表参照) n: ローテート/シフト命令のカウント、ENTER命令のレキシカルレベル、またはストリング操作命令の繰り返し数 m: ジャンプ先の命令のバイト数(80286の場合)、または構成要素数(80386の場合) UV,U,V: Pentiumのペアリング可能性は、UV(両方のパイプで可能)、U(Uパイプでのみ可能)、V(Vパイプでのみ可能)、無印(不可能)で表す。

16ビットアドレシングの追加バイト数と追加クロック数 offset: オフセットアドレス(16ビット) disp: 変位(8/16ビット) Bytes 8086 V30 80286 80386 80486 Pentium [BX] [SI] [DI] 0 5 0 0 0 0 0 [offset] 2 6 0 0 0 0/1 0 [BX+SI] [BP+DI] 0 7 0 0 1 1 0 [BX+DI] [BP+SI] 0 8 0 0 1 1 0 [BX+disp] [BP+disp] [SI+disp] [DI+disp] 1/2 9 0 0 0 0/1 0 [BX+SI+disp] [BP+DI+disp] 1/2 11 0 1 1 1 0 [BX+DI+disp] [BP+SI+disp] 1/2 12 0 1 1 1 0

32ビットアドレシングの追加バイト数と追加クロック数 base: ベースレジスタ(汎用レジスタ) index: インデックスレジスタ(ESPを除く汎用レジスタ) offset: オフセットアドレス(32ビット) disp: 変位(8/32ビット) scale: スケール(1,2,4,8) Bytes 80386 80486 Pentium [base] baseはESP,EBP以外 0 0 0 0 [offset] 4 0 0/1 0 [base+disp] baseはESP以外 1/4 0 0/1 0 [base+index*scale] baseはEBP以外 1 1 1 0 [offset+index*scale] 5 0 1 0 [base+index*scale+disp] 2/5 1 1 0 [base]#1 baseはEBP以外 1 0 0 0 [offset]#2 5 0 0/1 0 [base+disp]#1 2/5 0 0/1 0 #1 普通はbaseがESPの場合のみ使うエンコーディング #2 普通は使わないエンコーディング

プリフィックス 8086 V30 80286 80386 80486 Pentium REP REPZ REPNZ - - - - (1) (1) ES: CS: SS: DS: FS: GS: 2 2 (0) (0) (1) (1) LOCK 2 2 (0) (0) (1) (1) size (0) (1) (1) 0Fh(Jcc以外) - (0) (0) (1) (1)# (0) デコードに追加クロックが必要だが、普通は実行時間に隠される。 (1) デコードに1クロック追加されるが、前の命令が2クロック以上かかるときは隠れる。 # MMX対応Pentiumでは追加クロックはない。

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倍