この文章では、アセンブリ言語で書かれたプログラムをさらに高速化し、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」とだけ書くことにする。
最適化の基本は、実行される命令数を少なくしたり、実行時間(クロック数)の短い命令を使ったりして、プログラムの実行に必要な合計のクロック数を減らすことである。実行される命令数を少なくすることはたいてい、プログラム中の命令数を少なくすることになるし、実行時間の短い命令を使うことはたいてい、短い(バイト数の少ない)命令を使うことになるので、実行時間の最適化は、同時にプログラムのサイズの最適化になることが多い。
例えば、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以降)が上がる。だから、繰り返し何度も実行されるような、速度的に重要な部分だけは、実行に必要なクロック数に注目して最適化し、それ以外は、主にコードの長さ(バイト数)に注目して最適化するのがよい。
Bytes 8086 80286 80386 80486 MOV BX,0 3 4 2 2 1 XOR BX,BX 2 3 2 2 1 XORの代わりにSUBでも同様
Bytes 8086 80286 80386 80486 CMP BX,0 3 4 3 2 1 TEST BX,BX 2 3 2 2 1 TESTの代わりにANDまたはORでも同様
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とするのがよい。頻繁に調べるなら、レジスタに0を入れておいて使うとよい。
例 ;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と即値の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特別な擬似命令で、この最適化を自動的に行うアセンブラもある(TASM3.0のMASKFLAG,SETFLAG,FLIPFLAG,TESTFLAGなど)。80486とPentiumProでは部分レジスタストールに注意する。
逆に、レジスタ全体についての命令を使って、部分レジスタを二つ同時に変更することができる。例えば、桁上がりがなければ、 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ビットの変位(定数)アドレスの計算の際、限定されてはいるが、レジスタ同士や、レジスタと変位、レジスタ同士と変位の加算ができるので、これを利用して命令数を減らすことができる。例えば次のようなときである。
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である点も特殊なので、注意して使うべきである。
Bytes 8086 80286 80386 80486 MOV CX,BX / ADD CX,8 5 6 5 4 2 LEA CX,[BX+8] 3 11 3 2 18086では遅くなるので注意する。使えるレジスタの組合せに制限があるが、[BX+SI]のようなレジスタ同士の加算や、[BX+SI+8]のような3オペランドの加算もできる。
単純に、あるアセンブラシンボルのオフセットアドレスをレジスタに入れるなら、LEA命令ではなくMOV命令を使うべきである。
Bytes 8086 80286 80386 80486 LEA BX,[ADDR] 4 8 3 2 1 MOV BX,OFFSET ADDR 3 4 2 2 1このコードをアセンブリ言語で書く場合、次の点に注意する。
MOV BX,OFFSET DGROUP:ADDRのように、グループ名を明記する必要がある。ASSUME擬似命令でセグメントレジスタがそのグループを指していることを宣言している場合、そのセグメントレジスタを使って、
MOV BX,OFFSET DS:ADDRのように書いてもよい。
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 9CLの値を変えたくないときには、後者が2バイト長い欠点は相殺される。
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
Bytes 8086 80286 80386 80486 SHL AX,8 3 13 3 3 MOV AH,AL / XOR AL,AL 4 5 4 4 3# # 部分レジスタストールのペナルティーを含む
Bytes 8086 80286 80386 80486 SHR AX,8 3 13 3 3 MOV AL,AH / XOR AH,AH 4 5 4 4 3# # 部分レジスタストールのペナルティーを含む
Bytes 8086 80286 80386 80486 SAR AX,8 3 13 3 3 MOV AL,AH / CBW 3 4 4 5 4
ローテート/シフト命令と加算命令を置き換えると、命令の長さが同じで時間が短くなることがある。8086と80386以降では実行時間が逆転するので注意する。
Bytes 8086 80286 80386 80486 ADD AX,AX 2 3 2 2 1 SHL AX,1 2 2 2 3 3
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:で、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ルーチンを呼ぶときには、次の置き換えが可能である。
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を実行できるぶん速いかもしれない。
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命令で操作するとよい。
レジスタ毎に性格が異なるため、以下に示すレジスタの特徴を考慮して、値を割り当てるレジスタを決める。
CやPascalなどのコンパイル結果では、スタックフレームにローカル変数を置き、BPを使ってアクセスする方法がよく使われる。しかし、最初からアセンブリ言語で書かれたプログラムでは、普通はスタックフレームを使わないので、BPを、カウンタ、真偽値、一時記憶などに使うことができる。
サブルーチンに引数を渡したり、サブルーチンから結果を受け取ったりするときには、なるべくレジスタを使う。どのレジスタを使うかは、いっしょに使う他のルーチンとの兼ね合いで決める。アセンブリ言語では、サブルーチンをどこから呼び出すかはすべて把握できるので、重要な呼び出し場所で都合がよいように、レジスタの割り当てを決めればよい。
場合によっては、セグメントレジスタを値の一時記憶として使ってもよい。ただし、80286以降のプロテクトモードでは、セグメントレジスタに好きな値を入れることはできない。
MOV CX,10 L1: PUSH CX ; ; いろいろな処理 ; POP CX LOOP L1PUSHするレジスタと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命令を使う必要がない。
フラグにはこの他にも、いろいろな使い道がある。
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: RETALが変化してよければ、
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のどちらかがセットされていたらジャンプ)のように、フラグを複合して調べる命令もある。これを使うと便利な場合もある。
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などがあるので、レジスタの値を変えてよいか、どちらでセットされるのが都合よいかに応じて選ぶ。
ADD AX,CX RCR AX,1とすればよい。
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以降ではキャッシュを有効に使うことになる。ただし、誤って同時に使ってしまわないように、常に気をつける必要がある。
プリフィックスのうち、セグメントオーバーライドプリフィックスは、プログラムの工夫でなくすことができる。多くの命令では、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 SUB BP,CX L2: SHL CX,1 SHL CX,1 SHL CX,1 REP STOSW TEST BP,BP JNZ L1速度的にそれほど重要でなければ、次のようにするのもよい。
XOR AX,AX MOV DI,AX L1: 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では、「処理」の先頭でメモリ上のデータ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
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とすればよいし、AXの3倍をBXに加えるには、 ADD BX,AX を3回繰り返せばよい。AXを257倍するなら MOV AH,AL である。80186以降では、80186で追加された、即値を含む3オペランドのIMUL命令(演算数と演算結果が同じビット数なので、フラグレジスタを無視すればMUL命令としても使える)を使うほうが便利なこともある。
時間のかかる演算の結果は、あらかじめ計算しておいて、メモリ上に置いておいたほうがよいこともある。例えば、円を描くための三角関数の値などは、メモリ上にテーブルを作っておいたほうがよい。
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のような命令列を使う。
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
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命令を使うこともある。
Bytes 8086 80286 80386 80486 MOV AX,BX 2 2 2 2 1 XCHG AX,BX 1 3 3 3 3
ビットマップの操作などで、用意したマスクのビットが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のようにしたほうが速く短い。
上記のようにしてポリゴンなどを描画するには、与えられた範囲のマスクを生成する必要がある。 下位の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,8000H SHR AX,CL NEG AX上位のCL+1桁が0で残りは1
MOV AX,0FFFFH SHR AX,CL上位のCL+1桁が1で残りは0
MOV AX,8000H SAR AX,CL
PC-9800シリーズのグラフィックVRAMのように、バイト毎にmsbが左端に対応しているような場合には、バイト単位でマスクを作るか、ワード単位のマスクを作ってから、上下のバイトを入れ替えればよい。
例えば、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
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に入れる場合を仮定する。
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
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のようにすると、繰り返し中に実行される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つの値を交代で使いたいときは、次のようにする。
ループ外: MOV AX,a MOV CX,a XOR b ループ中: XOR AX,CX XOR CX,AX XOR CX,a XOR b XOR c
ループ外: XOR CX,AX XOR DX,CX ループ中: XOR AX,CX XOR CX,AX XOR CX,DX
ループ外: 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
ループ外: MOV AX,a MOV CX,a XOR b ループ中: XOR AX,CX XOR CX,a XOR c ; XOR DX,b XOR d と同じ
ループ外: 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を使う。ビットに関する操作では、8または16ビットぶんまとめるとだいぶ高速になることがある。このときは、バイトまたはワード境界を考慮してループを三つに分け、
ループではないが、再帰呼び出しされるサブルーチンで、引数が1ずつ増加または減少するものも、引数について展開することがある。こうすると、引数のためのレジスタも節約できるし、引数に関する演算を先にすませることもできる。
書き換えた命令が既にフェッチされていた場合、書き換え前の命令が実行されてしまう。特に80386や80486では、プリフェッチキューが大きいので、このようなことが起こりやすい。対策は、命令を書き換えてから実行するまでの間に、IPを変更する命令を実行して、キューをフラッシュすることである。
書き換えて余った場所に何もしない命令を入れる必要があることがある。1バイトならNOPがよさそうだが、80386以前だと3クロックかかるので、フラグが変化してよいなら、2クロックですむCLCなどがよい。2バイトなら、 MOV AL,AL などがよい。3バイトなら、フラグが変化するが、 TEST AX,0 などがよい。
複数の種類のCPUを対象にするときには、最も低レベルのCPUの持つ命令セットの範囲でコードを書く。普通は、コードが短くなるように、または最も低レベルのCPUで速くなるように最適化するが、特に高速にしたい場合には、CPUの種類を見分けて、それぞれのCPUに適したコードを実行するようにしたほうがよい。
CPUの種類を判別するには、次のようにする。
準備中
判別には時間がかかるので、判別結果はメモリに格納しておいて、必要な場所で場合分けに使う。判別したときにコードの一部を直接書き換えたり、実行時にコード生成したりする方法も使われる。
特に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ビットの変位(定数)注意するべき点は次の通りである。
16ビットモード(リアルモードと仮想8086モードを含む)で32ビットデータを扱う命令を使うと、命令の前に1バイトのオペランドサイズプリフィックスがつく。また、32ビットのアドレシングモードを使うと、命令の前に1バイトのアドレスサイズプリフィックスがつく。このため、16ビットモードで32ビットのデータやアドレシングモードを使うのは、32ビットが本当に必要な場合に限るべきである。
32ビットプロテクトモードでは、32ビットのデータやアドレシングモードを使ってもサイズプリフィックスはつかない。8ビットのデータもオペランドサイズプリフィックスを必要としない。逆に、16ビットのデータやアドレシングモードを使うと、プリフィックスがつく。だから、32ビットモードではなるべく16ビットのデータやアドレシングモードを使わず、8,32ビットのデータと32ビットのアドレシングモードを使うとよい。
80386以降のプロセッサで、プリフィックスつきの命令を使った場合の影響は、次の通りである。ここで、プリフィックスには、先頭バイトが0Fhの命令の0Fhも含む。
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に入るので注意する。
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命令を使うと、コードが短くなったりワークレジスタが必要なくなったりするので便利である。
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などとする必要はなく、 BT [SI],CX でよい。
80386には他にも追加された命令があるが、普通のプログラムではあまり使わない。
例: 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ビットレジスタに入れたい場合である。
Bytes 80386 80486 MOV EAX,3 6 2 2# LEA EAX,DS:[3] 5 2 2# # サイズプリフィックスのデコードのための追加クロックを含む
Bytes 80386 80486 MOV EBX,OFFSET V 6 2 2# LEA EBX,[V] 5 2 2# # サイズプリフィックスのデコードのための追加クロックを含む
80386では、RCL,RCR命令は遅くなったので、なるべく使わないほうがよい。80486以降でも、回数が1以外のRCL,RCR命令は遅いので、使わないほうがよい。
コードのアラインメントは、プロセッサによって事情が異なり、80386以降で共通するやり方はない。強いていうなら、80386は元々遅いのでアラインメントによる遅れの寄与は小さいとみなして、80486に合せておけばよいだろう。
; 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 ; 次のコード
この状態を利用するメリットは、次の通りである。
MOV EAX,80000000H SAR EAX,CL BSWAPのようにして作ったマスクパターンを使えばよい。
リピートプリフィックスなしのストリング操作命令は遅いので、速度が要求されるところではMOV命令とINC命令などの組合せに置き換えたほうがよい(ストリング操作命令を参照)。そうすれば、Pentiumではペアリングも可能になる。
リピートプリフィックスつきのストリング操作命令は、繰り返し回数が少ないときのオーバーヘッドが大きいので、注意して使う。80486ではREP LODSとREP STOS、PentiumではREP LODSとREP SCASとREP CMPSは、普通の命令の組合せに置き換えたほうが速い。
JCXZ,LOOP命令などは遅いので、普通の命令の組合せに置き換えたほうが速い。
MOVZX命令は高速化されなかったので、先にレジスタ全体をクリアしてからMOV命令を使うほうがよい。そうすれば、Pentiumではペアリングも可能になる。
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は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を読み出してもストールは発生しない。
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命令に即値オペランドがある場合に限り、次の命令でアドレス生成インターロックが発生する。
ペアにできる命令は次の通りである。
複雑そうだがペアにできる命令としては、LEA命令と、CALL(near直接)命令が挙げられる。逆に、ペアにできそうでできない命令としては、NEG,NOT,BSWAP命令と、TEST命令の一部の形式が挙げられる。
連続する二つの命令がペアになる条件は次の通りである。
文献3には、7バイトを越える命令はペアにできないと書かれているが、プリフィックスなしでは上の条件のほうが強く、7バイトの命令にプリフィックスをつけて8バイトにしてもペアにできたので、これは誤りであろう。
ペアになった命令の実行は、普通は、クロック数が多いほうの命令のクロック数ぶんかかる。ただし、ある場合には、余計なクロックを必要とすることがある。
ペアリングを有効に使うためには、次のようなことに注意する。
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とする。
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に登録されている分岐先のアドレスにある命令(分岐しないと予測したときは次の命令)のフェッチとデコードを始めることで、分岐命令は高速に実行される。予測が失敗すると、フェッチとデコードをやり直すので、次の命令の実行開始まで時間がかかる。
このため、次の点に注意してコードを書くとよい。
FSTSW AX SAHFの置き換えとして利用できる。
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クロックのストールが発生する。
ADD EAX,ECX / ADC EDX,0 ADD EAX,ECX / LEA EBX,[EDX+1] / CMOVC EDX,EBX
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が偶数のときは省いてよいなど、計算の手間を減らす方法がいろいろあるが、本題からはずれるので省略する。
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:
最初の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 E3XOR命令は単独で、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クロックで実行される。
次の関数は、この章の始めで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の最適化は、まだ不十分である。
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
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クロック以上かかるときは隠れる。
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倍