-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathasm.htm
2696 lines (2268 loc) · 138 KB
/
asm.htm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<HTML>
<HEAD>
<TITLE>fnami: Assembler</TITLE>
</HEAD>
<BODY>
<H1>アセンブラでの高速化</H1>
<OL>
<LI><A HREF="#INTRO">はじめに</A>
<LI><A HREF="#COMMON">共通/80286以前</A>
<LI><A HREF="#386">80386以降</A>
<LI><A HREF="#486">80486以降</A>
<LI><A HREF="#586">Pentium</A>
<LI><A HREF="#686">PentiumPro以降</A>
<LI><A HREF="#P4">Pentium4</A>
<LI><A HREF="#EXAMPLE">高速化の例</A>
<LI><A HREF="#CLOCK">主な命令一覧</A>
<LI><A HREF="#BIB">参考文献</A>
</OL>
<HR>
<H2><A NAME="INTRO">はじめに</A></H2>
アセンブリ言語でプログラムを書くということは、プログラムに最高の自由度と性能を与えるということである。アセンブリ言語ではコンピュータの持つ機能をすべて使えるので、高級言語では不可能だった処理が可能になるし、高級言語からは見えないCPUの機能を利用してプログラムを高速にしたり、プログラムを小さくしたりすることができる。特にインテルの8086とその後継プロセッサ(86系プロセッサ)の場合、アセンブリ言語で書き直すだけでプログラムが大幅に小さく、そして速くなることは多い。しかし、ただ単純にアセンブリ言語で書いただけでは、コンピュータはその本来の力を発揮しているとは言い難い。コンピュータには、もっと速く実行する可能性が秘められているのである。
<P>この文章では、アセンブリ言語で書かれたプログラムをさらに高速化し、CPUの持つ性能を十分に発揮させる方法を説明する。高級言語で書かれたプログラムの高速化に関して言われているような点について重複して述べることはしない。つまり、
<UL>
<LI>アルゴリズムを工夫して計算量を減らす。
<LI>無駄な計算をしない。
<LI>何度も使う値は先に計算しておく。
<LI>実行に時間のかかっている部分を見つけてそこを最適化する。
<LI>内側のループに注目して最適化する。
</UL>
などについては特に解説しない。
<P>86系CPUには、8086、8088、80186、80286、80386、80486、Pentium、PentiumProなど、および各種の互換プロセッサがある。この文章では、8086、80286、80386、80486、Pentium、PentiumProを扱っている。次のように分けて解説しているので、CPUに応じて、必要な章を読んでほしい。
<UL>
<LI><A HREF="#COMMON">共通/80286以前</A>
<LI><A HREF="#386">80386以降</A>
<LI><A HREF="#486">80486以降</A>
<LI><A HREF="#586">Pentium</A>
<LI><A HREF="#686">PentiumPro</A>
</UL>
例えば、PentiumProの場合には、共通、80386以降、80486以降、PentiumProの章を読んでほしい。なお、8088はデータバス幅が半分の8086、80386SXはデータバス幅が半分の80386とみなせるので、メモリアクセスに追加クロックが必要(8088は4クロック以上、80386SXは2クロック以上、メモリアクセスのウェイト数による)な他は、8086,80386と傾向は同じである。
<P>この文章では、レジスタを次のように表記する。文中であっても「AXレジスタ」などとは書かずに単に「AX」とだけ書くことにする。
<DL>
<DT>8ビットレジスタ
<DD>AL,CL,DL,BL,AH,CH,DH,BH
<DT>16ビットレジスタ
<DD>AX,CX,DX,BX,SP,BP,SI,DI
<DT>32ビットレジスタ
<DD>EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI
<DT>セグメントレジスタ
<DD>ES,CS,SS,DS,FS,GS
<DT>命令ポインタ(16,32ビット)
<DD>IP,EIP
<DT>フラグレジスタ(16,32ビット)
<DD>FLAGS,EFLAGS
<DT>個々のフラグ
<DD>CF,PF,AF,ZF,SF,TF,IF,DF,OF
</DL>
<HR>
<H2><A NAME="COMMON">共通/80286以前</A></H2>
この章では、86系CPUに共通する最適化と、80286以前のCPUに特有の最適化について述べる。80386以降では逆効果になるものについては、それぞれ注意書きをつけてある。
<P>最適化の基本は、実行される命令数を少なくしたり、実行時間(クロック数)の短い命令を使ったりして、プログラムの実行に必要な合計のクロック数を減らすことである。実行される命令数を少なくすることはたいてい、プログラム中の命令数を少なくすることになるし、実行時間の短い命令を使うことはたいてい、短い(バイト数の少ない)命令を使うことになるので、実行時間の最適化は、同時にプログラムのサイズの最適化になることが多い。
<P>例えば、BXが0かどうか調べる場合、普通に
<PRE> CMP BX,0
</PRE>
とすると、3バイト必要だが、
<PRE> TEST BX,BX
</PRE>
とすれば、2バイトですむ。また、実行に必要なクロック数は、次のように、短縮されるか同じになる(Bytesの下の数字は命令のバイト数を、8086などの下の数字は、各プロセッサで実行に必要な最小のクロック数を表す)。
<PRE> Bytes 8086 80286 80386 80486
CMP BX,0 3 4 3 2 1
TEST BX,BX 2 3 2 2 1
</PRE>
この二つの命令は、AFを除いて同じ動作をするので、たいていの場合は前者を後者で置き換えると改良になる。TEST命令の代わりに、AND命令またはOR命令を用いても同じである。
<P>命令自体の実行時間が同じでも、あるいはむしろ長くなっても、プログラムを短くすることは有利な場合が多い。短いプログラムは、ディスクなどからメモリに読み込む時間が短くてすみ、CPUが命令をメモリからフェッチする時間が短くなり、キャッシュの使用効率(80486以降)が上がる。だから、繰り返し何度も実行されるような、速度的に重要な部分だけは、実行に必要なクロック数に注目して最適化し、それ以外は、主にコードの長さ(バイト数)に注目して最適化するのがよい。
<H3>0に関する操作</H3>
定数0に関係する操作は、特別な命令(列)を使うことで、一般の定数の場合より短いコードにできることが多い(もちろん、0の加算などはすっかり省略してよい)。これには、次のようなものがある。
<UL>
<LI>BXを0にする(フラグが保存されなくてよい場合)
<PRE> Bytes 8086 80286 80386 80486
MOV BX,0 3 4 2 2 1
XOR BX,BX 2 3 2 2 1
XORの代わりにSUBでも同様
</PRE>
<LI>BXと0を比較する(AFは不要の場合)
<PRE> Bytes 8086 80286 80386 80486
CMP BX,0 3 4 3 2 1
TEST BX,BX 2 3 2 2 1
TESTの代わりにANDまたはORでも同様
</PRE>
<LI>メモリ上のワードデータを0にする(AXとフラグが保存されなくてよい場合)
<PRE> Bytes 8086 80286 80386 80486
MOV [ADDR],0 6 16 3 2 2#
XOR AX,AX / MOV [ADDR],AX 5 13 5 4 2
# オフセットと即値の<A HREF="#EXTRA">追加クロック</A>を含む
</PRE>
続けていくつものデータを0にする場合は、かなりバイト数の節約になる。なお、連続したデータ領域を0で埋めるには、REP STOS命令を使うとよい。
</UL>
<P>なお、メモリ上のデータが0かどうか調べるには、素直に
<PRE> CMP [ADDR],0
</PRE>
とするのがよい。頻繁に調べるなら、レジスタに0を入れておいて使うとよい。
<H3>INC,DEC命令</H3>
INC,DEC命令は、オペランドに1を加減算する命令で、ADD,SUB命令の特別な場合(1または2バイト短い)として使えるが、INC,DEC命令ではCFが変化しないことを利用すると、応用範囲が広がる(PentiumProでは<A HREF="#STALL">ストール</A>に注意)。例えば、多倍長の加算をするときにCFを別に保存する必要がない。
<PRE>例
;SIが指す多倍長数を、DIが指す多倍長数に加える
;多倍長数の長さはCXワード
CLC
L1: LODSW
ADC [DI],AX
INC DI
INC DI
LOOP L1
JC L2 ; オーバーフローしたときの処理
</PRE>
逆に、CFを変化させる必要があるときは、ADD,SUB命令を使う。
<P>ワードレジスタに対するINC,DEC命令は1バイトですむので、コードを短くするのに便利である。ワードレジスタに対する2までの加減算はINC,DEC命令を使うとよい。ただし、少し遅くなる。
<PRE> Bytes 8086 80286 80386 80486
ADD BX,2 3 4 3 2 1
INC BX / INC BX 2 6 4 4 2
</PRE>
<P>バイトレジスタに対してINC,DEC命令を使いたいときも、ワードレジスタに対する命令を使ったほうがよいこともある。例えばBXの下位バイトが0FFHでないことがわかっている場合、INC BLの代わりにINC BXを使うと1バイト短くなる。
<P>オペランドの値が0であることがわかっているとき、INC命令で1にしたり、DEC命令で0FFFFhにしたりすることができる。
<PRE> Bytes 8086 80286 80386 80486
MOV AX,1 3 4 2 2 1
INC AX 1 3 2 2 1
</PRE>
これは、レジスタを真偽値として使うときに便利である。
<P>オペランドの値が変化してよいときには、INC命令で0FFFFhかどうか調べたり、DEC命令で1かどうか調べたりすることができる。
<PRE> Bytes 8086 80286 80386 80486
CMP AX,1 3 4 3 2 1
DEC AX 1 3 2 2 1
</PRE>
続けて使うと0FFFEh,0FFFDh,0FFFCh,…、あるいは2,3,4,…かどうか調べることもできる。
<H3>アキュームレータ</H3>
アキュームレータ(AL,AX,EAX)に関する命令には、他のレジスタを使う場合に比べて1バイト短いエンコーディングを持つものがある。これを使っても命令自体のクロック数はたいてい変わらないが、コードを短くすることができる。これには、次のものがある。
<PRE>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クロック短縮)
</PRE>
だから、このような演算をよくするデータは、なるべくアキュームレータに入れておくのがよい。
<H3>即値</H3>
即値とのADD,ADC,SUB,SBB,AND,OR,XOR,CMP命令は、即値の値が-128~+127のときは、範囲外のときに比べて1バイト短いエンコーディングを持つ。これを使っても命令自体のクロック数は変わらないが、コードを短くすることができる。なお、上で述べた、アキュームレータに関する短縮形には、このエンコーディングはない。即値とのTEST命令にもこのエンコーディングはないが、部分レジスタを使うことで同じ効果を得られる。
<P>レジスタの上位バイトまたは下位バイトだけに関する、即値との論理演算は、部分レジスタを使うと1バイト短くなる。
<PRE>例
AND BX,0FFFEH → AND BL,0FEH (フラグは異なる)
OR BX,8000H → OR BH,80H (フラグは異なる)
TEST BX,0006H → TEST BL,06H
</PRE>
特別な擬似命令で、この最適化を自動的に行うアセンブラもある(TASM3.0のMASKFLAG,SETFLAG,FLIPFLAG,TESTFLAGなど)。80486とPentiumProでは<A HREF="#PARTIAL">部分レジスタストール</A>に注意する。
<P>逆に、レジスタ全体についての命令を使って、部分レジスタを二つ同時に変更することができる。例えば、桁上がりがなければ、 ADD AX,0FF01H は、ALに1を加え、AHから1を減ずる。
<H3>アドレシングモード</H3>
80286以前では、メモリを参照するアドレシングモードは、次の16通り(変位の大きさを入れると24通り)である。
<PRE>[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ビットの変位(定数)
</PRE>
アドレスの計算の際、限定されてはいるが、レジスタ同士や、レジスタと変位、レジスタ同士と変位の加算ができるので、これを利用して命令数を減らすことができる。例えば次のようなときである。
<UL>
<LI>オフセットBX+4のメモリを読んでAXに入れる
<PRE> 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
# <A HREF="#AGI">アドレス生成インターロック</A>のペナルティーを含む
</PRE>
BXの値を変化させたくない場合は特に、後者が有利である。
</UL>
不必要に複雑なアドレシングモードを使うと遅くなるので注意する(<A HREF="#CLOCK">命令一覧</A>の「16ビットアドレシングの追加バイト数と追加クロック数」を参照)。
<P>アドレス指定で変位を使う場合、変位が8ビットで収まる(-128~+127)ときには、範囲外のときに比べて1バイト短くなる。変位が0のときは変位なしとみなされて、2バイト短くなり、場合によっては実行時間も短縮される。
<P>ただし、BP間接だけは、変位なしのエンコーディングがないので、変位が128~+127のときに1バイト短くなるだけである。この他、BPを含むアドレシングモード([BP+SI]なども含む)は、デフォルトのセグメントがSSである点も特殊なので、注意して使うべきである。
<H3>LEA命令</H3>
LEA命令は、オペランドのメモリをアクセスするのではなく、そのオフセットアドレスをレジスタに入れる命令である。この命令の利点は、次の例のように、アドレス計算の結果を任意のレジスタに入れられることである。
<UL>
<LI>BXと即値の加算結果をCXに入れる
<PRE> 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
</PRE>
8086では遅くなるので注意する。使えるレジスタの組合せに制限があるが、[BX+SI]のようなレジスタ同士の加算や、[BX+SI+8]のような3オペランドの加算もできる。
</UL>
<P>単純に、あるアセンブラシンボルのオフセットアドレスをレジスタに入れるなら、LEA命令ではなくMOV命令を使うべきである。
<UL>
<LI>オフセットアドレスをBXに入れる
<PRE> Bytes 8086 80286 80386 80486
LEA BX,[ADDR] 4 8 3 2 1
MOV BX,OFFSET ADDR 3 4 2 2 1
</PRE>
このコードをアセンブリ言語で書く場合、次の点に注意する。
<UL>
<LI>ADDRがGROUP擬似命令でグループ化されたセグメント内のシンボルの場合、
<PRE> MOV BX,OFFSET DGROUP:ADDR
</PRE>
のように、グループ名を明記する必要がある。ASSUME擬似命令でセグメントレジスタがそのグループを指していることを宣言している場合、そのセグメントレジスタを使って、
<PRE> MOV BX,OFFSET DS:ADDR
</PRE>
のように書いてもよい。
<LI>アセンブラによっては、LEA命令を使って書いても、自動的にMOV命令を使ったコードを出力してくれる(TASM 2.0など)。その場合には、上記の問題も同時に解決するので便利である。
</UL>
</UL>
<H3><A NAME="ROTATE">ローテート/シフト</A></H3>
8086では、1以外の定数によるローテート/シフト命令を持たないため、CLで回数を指定する形式を使いたくなるが、ローテート/シフト命令を繰り返したほうがよいことが多い。
<UL>
<LI>AXを左に3回ローテート
<PRE> 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
</PRE>
CLの値を変えたくないときには、後者が2バイト長い欠点は相殺される。
</UL>
80186以降では、定数の値によって、 ROL AX,n と、n回の ROL AX,1 を使い分けるとよい。なお、8回ローテート/シフトするには、部分レジスタを使うとよい。
<UL>
<LI>AXを左または右に8回ローテート(フラグは不要の場合)
<PRE> 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
</PRE>
<LI>AXを左に8回シフト(フラグは不要の場合)
<PRE> Bytes 8086 80286 80386 80486
SHL AX,8 3 13 3 3
MOV AH,AL / XOR AL,AL 4 5 4 4 3#
# <A HREF="#PARTIAL">部分レジスタストール</A>のペナルティーを含む
</PRE>
<LI>AXを右に8回シフト(符号なし)(フラグは不要の場合)
<PRE> Bytes 8086 80286 80386 80486
SHR AX,8 3 13 3 3
MOV AL,AH / XOR AH,AH 4 5 4 4 3#
# <A HREF="#PARTIAL">部分レジスタストール</A>のペナルティーを含む
</PRE>
<LI>AXを右に8回シフト(符号つき)(フラグは不要の場合)
<PRE> Bytes 8086 80286 80386 80486
SAR AX,8 3 13 3 3
MOV AL,AH / CBW 3 4 4 5 4
</PRE>
</UL>
80386以降ではかえって遅くなることがあるので注意する。
<P>ローテート/シフト命令と加算命令を置き換えると、命令の長さが同じで時間が短くなることがある。8086と80386以降では実行時間が逆転するので注意する。
<UL>
<LI>AXを2倍にする(AFは不要の場合)
<PRE> Bytes 8086 80286 80386 80486
ADD AX,AX 2 3 2 2 1
SHL AX,1 2 2 2 3 3
</PRE>
<LI>AXをキャリーつきで2倍にする(PF,AF,ZF,SFは不要の場合)
<PRE> Bytes 8086 80286 80386 80486
ADC AX,AX 2 3 2 2 1
RCL AX,1 2 2 2 9 3
</PRE>
</UL>
<P>オペランドの値が0または1であることがわかっているとき、SHR命令を使うと、オペランドを0にすると同時に、元のオペランドをCFにコピーすることができる。真偽値のクリアとテストを同時に行うのに便利である。真偽値をクリアでなくセットしたければ、続けてINC命令を使う(INC命令はCFを変更しない)。
<P>80186以降では、オペランドの符号を値として設定したいときに、SHR,SAR命令を使うとよい。
<PRE>負なら1 SHR AX,15
負なら-1 SAR AX,15
</PRE>
<H3><A NAME="JUMP">ジャンプ</A></H3>
<P>ジャンプ命令は、実行時間のかかる命令なので、なるべく使わないようにする。条件ジャンプを使う場合には、なるべくジャンプしなくてすむようにする。例えば、次のコード
<PRE> TEST BP,BP
JNZ L1
MOV AX,100
JMP L2
L1: MOV AX,200
L2:
</PRE>
で、BPが0でないことが多いなら、次のように置き換えるとよい。
<PRE> MOV AX,100
TEST BP,BP
JZ L2
MOV AX,200
L2:
</PRE>
Pentium以降では、分岐予測が成功すれば、ジャンプ命令の実行時間は問題ではなくなった。また、MMXなしPentiumの分岐予測では、条件ジャンプはなるべくジャンプするようにするとよい。
<P>フラグをうまく使うと、条件ジャンプ命令をなくすことができる(<A HREF="#FLAGS">フラグ</A>の節を参照)。
<P>無条件ジャンプのジャンプ先が1バイトまたは2バイト先で、フラグが変化してよいときは、JMP命令の代わりに、
<PRE> DB 0A8H ; TEST AL,n
</PRE>
または
<PRE> DB 0A9H ; TEST AX,nn
</PRE>
を使うとよい。こうすると、スキップしたい1バイトまたは2バイトの命令(列)をTEST命令の即値データとして使ってしまうので、フラグの変化以外は何もしないで、次の命令(ジャンプ先)の実行が始まることになる。ただし、Pentium以降では、命令キャッシュに命令の境界を記憶して、次回の実行で利用しているので、このようなコードは避けたほうがよい。
<P>ジャンプ先が同一セグメント内のJMP命令は、ジャンプ先が-128~+127バイトの範囲なら、短い形のエンコーディング(short形式)を利用できる。だから、ジャンプ先がなるべく近くになるようにルーチンを並べ変えれば、コードを短くすることができる。
<P>マルチパスのアセンブラ(TASMで/Mスイッチをつけたときなど)では、可能なら自動的にshort形式を使ってくれる。そうでないアセンブラを使うときには、オペランドの前に「SHORT」をつける。
<P>80286以前では、条件つきジャンプ命令にはshort形式しかないので、ジャンプ先が-128~+127バイトの範囲にない場合には、無条件ジャンプ命令と組み合わせる必要がある。マルチパスのアセンブラ(TASMで/Mスイッチをつけ、JUMPS擬似命令を使った場合など)では、必要なら自動的に、例えば
<PRE> JAE L3
</PRE>
を
<PRE> JNAE L99
JMP L3
L99:
</PRE>
にするような処理をしてくれる。だから、アセンブラに頼ってもよいのだが、もしこの例でほとんどの場合にL3にジャンプしないのなら、
<PRE> JAE TO_L3
</PRE>
として、近くの別のところに
<PRE>TO_L3: JMP L3
</PRE>
を置いたほうがよい。こうすると、同じラベルにとぶ別のジャンプ命令と共有することもできる。速度的に重要なコードの部分では、なるべくジャンプ先がshort形式で届くようにルーチンを配置し、届かないところはジャンプする割合を考えて、上のどちらかの形式に条件ジャンプを書き換えるようにするとよい。
<P>マルチパスでないアセンブラでも、マクロを使えば、上のL99を使う形式を自動的に生成できる(前方参照のラベルが128バイトまでに入った場合でもこうなってしまうが)。このとき生成した JMP L3 の位置もマクロで覚えておけば、以後の同じラベルにとぶジャンプ命令で、TO_L3の代わりに使える。
<H3>コール/リターン</H3>
<PRE> CALL SUB1
RET
</PRE>
のように、CALL命令とRET命令が続く場合は、
<PRE> JMP SUB1
</PRE>
に置き換えることができる。ただし、CALLとRETの種類(near/far)が同じでなければならない。
<P>条件つきRET命令はないので、条件ジャンプ命令と組み合わせて使う。
<PRE> JNAE L99
RET
L99:
</PRE>
のように、条件を反転させてもよいが、たいていはリターンしないのであれば、
<PRE> JAE L_RET
</PRE>
として、近くの別のところに
<PRE>L_RET: RET
</PRE>
を置くのがよい。もちろん、これは前のサブルーチンの終わりなどのRET命令と共有できる。ただし、MMXなしPentiumでは、RET命令の共有は分岐予測ミスによる速度低下の原因となることがある。
<P>速度的に重要な部分では、サブルーチンの呼び出しとリターンにかかる時間を節約するために、サブルーチン本体を呼び出し場所に埋め込む(インライン展開)とよい。長いサブルーチンなら、サブルーチンの中で頻繁に使われる部分だけを埋め込むこともできる。例えば、
<PRE>SUB1: CMP SI,[BUFFER_END]
JNE L1
;
; 長い処理
;
L1: LODSB
RET
</PRE>
のようなサブルーチンなら、CXにSIから[BUFFER_END]までのバイト数を入れることにして、
<PRE> LOOP L1
CALL SUB2
L1: LODSB
</PRE>
をサブルーチン呼び出し場所に埋め込み、
<PRE>SUB2: ;
; 長い処理
;
MOV CX,[BUFFER_END]
SUB CX,SI
RET
</PRE>
のようなサブルーチンを用意する。
<P>farルーチンは、呼び出しやリターンに時間がかかるので、なるべく使わないようにする。コードを複数のセグメントに分けて書かなければならない場合でも、なるべく同じセグメント内で処理がすむようにする。場合によっては、farコール用のエントリを別に作ったり、ルーチンを二つのセグメントにコピーしたりしたほうがよいこともある。
<P>同じセグメントのfarルーチンを呼ぶときには、次の置き換えが可能である。
<UL>
<LI>呼び出し元と先が同じセグメントのfarルーチンを呼ぶ
<PRE> 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
</PRE>
この最適化を自動的に行うアセンブラもある(TASM 2.0など)。8086では1クロック損するが、元が遅いのであまり気にしなくてよい。命令フェッチまで考えると、すぐにPUSH CSを実行できるぶん速いかもしれない。
</UL>
<H3>レジスタ</H3>
レジスタをオペランドにする命令は、メモリをオペランドにする命令と比べて、短く速い(<A HREF="#CLOCK">命令一覧</A>参照。メモリアクセスのウェイト数や、80486以降のキャッシュミスによっては、差が広がる)。また、レジスタにしかできない操作も多い。だから、計算に必要な値は、なるべくレジスタに入れておくのがよい。また、即値を使うよりレジスタを使ったほうが短く速いことが多いので(<A HREF="#CLOCK">命令一覧</A>参照。ローテート/シフト命令は例外)、同じ即値を頻繁に使うならレジスタに入れて使ったほうがよい。ただし、元々メモリにあるデータに1回だけ演算をするときには、メモリをオペランドにするほうがよい。例えば次のような場合である。
<UL>
<LI>オフセットBXのメモリのワードデータに3を加える。
<PRE> 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
</PRE>
</UL>
<P>86系CPUの汎用レジスタは、SPを除いて7個しかなく、アドレシングモードや、いくつかの命令では特定のレジスタしか使えないため、レジスタ割り付けは簡単ではない。しかし、よく考えれば多くの場合、速度が重要なコード部分で使っているほとんどすべての値をレジスタに置くことができる。
<P>AX,CX,DX,BXの四つのレジスタは、AL,AH,CL,CH,DL,DH,BL,BHのように二つずつに分けて使うことができる。値を1バイトで表現できるときには、これらのレジスタを使うとよい。1バイトでは表現できないように見えても、実際に必要なのは1バイトだけですむ場合もあるので、注意する。例えば、上位バイトが共通な二つの数を使う場合などである。
<P>1ビットで表現できるような値をたくさん使うときには、レジスタの各ビットをそれに割り当てて、AND,OR,XOR,TEST命令で操作するとよい。
<P>レジスタ毎に性格が異なるため、以下に示すレジスタの特徴を考慮して、値を割り当てるレジスタを決める。
<DL COMPACT>
<DT>AX<DD>乗除算の演算数や結果。ストリング操作命令のデータ。IN,OUT命令のデータ。他のレジスタに比べて、1バイト短いエンコーディングを持つ命令がある。
<DT>CX<DD>ループ命令やストリング操作命令のカウンタ。CLは、ローテート/シフト命令のカウンタ。
<DT>DX<DD>16ビット乗算の結果の上位が入る。16ビット除算の被除数の上位を入れる。16ビット除算の結果の剰余が入る。IN,OUT命令のポートアドレス。
<DT>BX<DD>バイトレジスタに分割できるレジスタのうち、アドレス指定に使える唯一のもの。
<DT>SP<DD>スタックポインタ。普通は他の用途には使わない。
<DT>BP<DD>アドレス指定に使えるが、デフォルトセグメントがSSなので注意。
<DT>SI<DD>アドレス指定に使える他、ストリング操作命令の転送元アドレスを指定する。
<DT>DI<DD>アドレス指定に使える他、ストリング操作命令の転送先アドレスを指定する。
</DL>
<P>CやPascalなどのコンパイル結果では、スタックフレームにローカル変数を置き、BPを使ってアクセスする方法がよく使われる。しかし、最初からアセンブリ言語で書かれたプログラムでは、普通はスタックフレームを使わないので、BPを、カウンタ、真偽値、一時記憶などに使うことができる。
<P>サブルーチンに引数を渡したり、サブルーチンから結果を受け取ったりするときには、なるべくレジスタを使う。どのレジスタを使うかは、いっしょに使う他のルーチンとの兼ね合いで決める。アセンブリ言語では、サブルーチンをどこから呼び出すかはすべて把握できるので、重要な呼び出し場所で都合がよいように、レジスタの割り当てを決めればよい。
<P>場合によっては、セグメントレジスタを値の一時記憶として使ってもよい。ただし、80286以降のプロテクトモードでは、セグメントレジスタに好きな値を入れることはできない。
<H3>スタック</H3>
86系CPUは、スタックに値を積んだり降ろしたりするための、専用の命令を持っている。特に、レジスタのPUSH,POP命令は1バイトですむ。レジスタが足りなくなったときには、しばらく使わない値をPUSHしておき、使う前にPOPするとよい。例えば、次のようにする。
<PRE> MOV CX,10
L1: PUSH CX
;
; いろいろな処理
;
POP CX
LOOP L1
</PRE>
PUSHするレジスタとPOPするレジスタは異なってもよい。ただし、PUSHする回数とPOPする回数が異なるとスタックポインタがずれてしまうので注意する。途中で条件ジャンプが入るときには、間違えやすいので特に注意する。
<P>PUSH,POP命令は、メモリをオペランドにすることもできる。メモリ上のデータをコピーするときに、空いているレジスタがなければ、PUSHとPOPを使ってスタック経由でコピーするとよい(80486以降では逆効果)。
<PRE> 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
</PRE>
<P>セグメントレジスタを直接コピーする命令はないので、普通は汎用レジスタを経由するが、スタックを経由するとコードが短くなる。
<PRE> 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
</PRE>
セグメントレジスタの値の交換も、スタックを経由すれば、汎用レジスタを使わずにできる。
<P>80186以降では、セグメントレジスタに即値を入れたいときにも、スタックを経由するとコードが短くなる。セグメントアドレスが1バイトで表せるときは、さらに1バイト短くなる。
<PRE> 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
</PRE>
<P>8086には、定数をPUSHする命令がないので、空いているレジスタに定数を入れてPUSHする。アセンブラによっては、8086用にアセンブルするモードで
<PRE> PUSH 999
</PRE>
と書くと、次のような10バイトの命令列
<PRE> PUSH AX
PUSH BP
MOV BP,SP
MOV [BP-2],999
POP BP
</PRE>を生成するものもある(TASM2.0など)が、遅いのであまり使わないほうがよい。
<P>サブルーチンの入口で、使うレジスタをPUSHし、出口でPOPすると、サブルーチン中でレジスタを、高級言語のローカル変数のように使うことができる。サブルーチンの再帰呼び出しにも対応できる。
<P>スタックポインタ(SP)に対して直接演算してもよい(80486以降では<A HREF="#AGI">アドレス生成インターロック</A>に注意)。例えば、 ADD SP,6 とすると、スタックに積んだ値を三つ捨てることができる。
<PRE> Bytes 8086 80286 80386 80486
POP AX / POP AX / POP AX 3 24 15 12 3
ADD SP,6 3 4 3 2 1
</PRE>
また、次のようにすれば、高級言語のコンパイラが生成するコードと同じように、まとまったワークエリアをスタック上にとって、BPを使ってアクセスすることができる。
<PRE> PUSH BP
MOV BP,SP
SUB SP,100
;
; 処理
;
MOV SP,BP
POP BP
RET
</PRE>
この方法も再帰呼び出しに対応できる。ただし、大きなワークエリアをとるときは、スタックがオーバーフローしないように注意する。
<P>スタックを、データの順序を反転するための一時記憶として使うこともできる。例えば、10進数表示ルーチンを、10での除算の繰り返しで実現する場合、桁の順序を反転する必要があるが、スタックを使うと次のように書ける。
<PRE>;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
</PRE>
<P>サブルーチンのリターンアドレスもスタックに積まれるので、スタックを操作する命令と組み合わせて使うことができる。例えば、nearサブルーチンで ADD SP,2 とすると、リターンアドレスを捨てることができるし、 POP BX とすると、リターンアドレスをBXに入れることができる。また、
<PRE> CALL SUB1
JMP SUB2
</PRE>
の代わりに
<PRE> PUSH OFFSET SUB2
JMP SUB1
</PRE>
を使うようなことができる。ただし、MMX対応PentiumおよびPentiumPro以降では、このような書き方はリターンアドレススタックの働きの妨げになるので、避ける。
<P>なお、
<PRE> PUSH BX
RET
</PRE>
とするよりは、 JMP BX のほうがよい。farジャンプのときには、専用の命令がないので、
<PRE> PUSH ES
PUSH BX
RETF
</PRE>
のようにするとよい。
<P>リロケート可能なルーチンで命令ポインタ(IP)の値を知りたいときには、CALL命令が相対アドレス指定であることを利用して、次のようにする。
<PRE> CALL L3
L3: POP BX
</PRE>
こうすると、L3の実際のオフセットアドレスがBXにはいる。
<P>サブルーチンの最後で別のサブルーチンを呼ぶ場合、
<PRE> CALL SUB
RET
</PRE>
ではなく
<PRE> JMP SUB
</PRE>
のほうが短く速い。テイルリカージョンをループに書き換えるのは、これの特別な場合と考えられる。
<P>複数のスタックを切り替えて使うときには、ときどきSSとSPを変更する必要がある。SSを変更する命令の直後(8086ではセグメントレジスタを変更する命令の直後)には、割り込み(NMIも含む)がかからないようになっているので、SS,SPの順に変更すればCLI命令を使う必要がない。
<H3><A NAME="FLAGS">フラグ</A></H3>
フラグレジスタ(FLAGS)のうち、演算に関係するCF,PF,AF,ZF,SF,OFをここで扱う。これらのフラグを有効に利用すると、高級言語から使っていたのでは考えられないような命令列を書くことができる。高級言語風に考えると、フラグの役割は次の通りである。
<UL>
<LI>演算結果が0か、正か、負か、符号つき/符号なしのオーバーフローなどを判断する。
<LI>比較の結果が等しいか、符号つき/符号なしで大きいか/小さいかなどを判断する。
</UL>
アセンブリ言語では、この他に、次のような役割もある。
<UL>
<LI>多倍長演算(ADC,SBB命令では、CFをキャリーまたはボローとして使う)
<LI>ローテート/シフト命令であふれたビットがCFにはいる。RCL,RCR命令は、CF経由でローテートする。多倍長のシフトや、ビット順反転に利用できる。
<LI>10進補正命令は、前の演算で設定されたAFの値を使う。
</UL>
<P>フラグにはこの他にも、いろいろな使い道がある。
<DL>
<DT>1ビットの値の受け渡し
<DD>CFやZF、場合によってはPF,SF,OFを値の受け渡しに使う。サブルーチンのエラーや、判定結果などを返すのに便利である。MS-DOSのファンクションリクエストにも、CFやZFで結果を返すものがある。受け側では普通、条件ジャンプ命令で処理を分けることになる。
<P>CFには専用のセット、リセット、反転命令があり、独立して操作できるが、他のフラグはそうではない。そのため、可能な状態を考えて値をフラグに割り当てる必要がある。ZFをセットするには、 CMP AL,AL を使うとよい(CF,SF,OFはリセットされる)。ZFをリセットするには、レジスタが変化するが、 OR AL,0FFh などを使うとよい(CF,OFはリセットされ、SFはセットされる)。CFは、他のフラグをいじるとリセットされてしまうことが多い(AND,OR,XOR,TEST命令では必ずリセットされる)ので、後でSTC命令を使ってセットするのもよい。
<P>フラグを設定するときに、わざわざそのための命令を使う必要は必ずしもない。例えば、リングバッファが空かどうかをZFに返すルーチンは、リングバッファの先頭と末尾のポインタを比較して、そのままリターンすればよい。また、ALに数字のASCIIコードが入っているかどうかをCFに返すルーチンは、次のようにすればよい。
<PRE> CMP AL,'0'
CMC
JNC NO
CMP AL,'9'+1
NO: RET
</PRE>
ALが変化してよければ、
<PRE> SUB AL,'0'
CMP AL,10
RET
</PRE>
でよい。このように、実際の処理で使いやすいようにフラグに値を割り当てるとよい。
<P>もし、値が0かどうかによって、CFを設定しなければならなくなったら、次のようにする。
<PRE>AXが0ならCFをセット CMP AX,1
AXが0以外ならCFをセット NEG AX
</PRE>
逆に、CFの値によってレジスタに0または0FFFFhを入れ、ZFをその通りに設定するには、 SBB AX,AX などを使う(CFは保存される)。
<P>条件ジャンプ命令には、JBE命令(CFとZFのどちらかがセットされていたらジャンプ)のように、フラグを複合して調べる命令もある。これを使うと便利な場合もある。
</P>
<DT>CFの値を使った演算
<DD>例えば、AXが100未満ならCXに1を加えたいなら、
<PRE> CMP AX,100
ADC CX,0
</PRE>
とすればよい。逆に、AXが100以上ならCXに1を加えたいなら、
<PRE> CMP AX,100
SBB CX,-1
</PRE>
とすればよい。二番目の命令の即値を適当な値にすれば、1だけ異なる二つの数を場合分けによって加減算できる。
<P>例えば、AXが100未満ならCXを30に、そうでなければ10にしたければ、
<PRE> CMP AX,100
SBB CX,CX
AND CX,20
ADD CX,10
</PRE>
とすればよい。定数の値によっては、AND命令やADD命令は不要である。
<P>今までの例では、CFをCMP命令でセットしたが、ADD,SUB命令や、ローテート/シフト命令などでもよい。特に、レジスタの値の符号によってCFを設定するときは、
<PRE>0以上ならセット CMP AX,8000H または SUB AX,8000H
負ならセット SHL AX,1 または ADD AX,AX または ADD AX,8000H
</PRE>
などがあるので、レジスタの値を変えてよいか、どちらでセットされるのが都合よいかに応じて選ぶ。
</P>
<DT>CFを最上位桁として利用
<DD>加減算の結果をすぐに2で割るときは、CFを最上位桁として利用できる。例えば、AXとCXの符号なし平均をとるときは、
<PRE> ADD AX,CX
RCR AX,1
</PRE>
とすればよい。
</P>
<DT>フラグの保存
<DD>フラグを設定する場所と使う場所の間にフラグを変化させる命令があるときには、フラグを保存する必要がある。普通はPUSHF,POPF命令を使う。AHが空いていて、OFを保存する必要がなければ、LAHF,SAHF命令を使うと、速く、何度も取り出せて便利である。AHが空いていなくても、例えばALが空いていて、CFだけ保存するのなら、 SBB AL,AL で保存して、 ROL AL,1 で取り出せばよい。80386と80486では、 ADD AL,AL で取り出したほうが速いが、8回までとなる。
<PRE> 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
</PRE>
<PRE> 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
</PRE>
</DL>
<P>86系CPUでは、MOV命令などの演算を行わない命令では、フラグは変化しない。これを利用すると、次のようなコードを書ける。
<PRE> TEST AX,AX
MOV AX,100
JZ L1
MOV AX,200
L1:
</PRE>
演算を行っても、フラグを変化させない命令もある。INC,DEC命令はCFを変化させない。ROL,ROR,RCL,RCR命令は、PF,AF,ZF,SFを変化させない。また、NOT命令はすべてのフラグを変化させない。LEA命令も、フラグを変化させない演算命令として使える。
<H3>メモリ</H3>
メモリアクセスは時間がかかるので、なるべくまとめて行うとよい。例えば次のように、
<PRE>A DB ?
B DB ?
</PRE>
二つのバイトデータAとBがメモリ上で並んでいるときは、
<PRE> MOV WORD PTR A,5*256+3 ; Aに3,Bに5
</PRE>
のようにまとめて初期化などをすることができる。
<P>メモリ上のバイトデータをワードレジスタにゼロ拡張して入れたいことが頻繁にあるなら、バイトデータの次のアドレスに0を入れておく。例えば、
<PRE>COUNT DB ?
DB 0
</PRE>
のように定義し、
<PRE> MOV COUNT,AL ; バイトデータの書き込み
MOV CX,WORD PTR COUNT ; ワードデータとして読み込み
</PRE>
のように使う。
<P>初期化の必要なワークエリアは、大きな領域を規則的に埋める場合を除き、初期化のコードを書くより、データそのものを書いたほうがよい。例えば、10の冪のテーブルが必要な場合、
<PRE> 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
</PRE>
などとするよりは、単に
<PRE>POW10 DW 1,10,100,1000,10000
</PRE>
で十分である。テーブルを作るための計算が複雑で、暗算ではできないときには、別のプログラムを作って先に計算して使う。アセンブラのマクロを使って計算してもよい。
<P>同時に使うことのない二つのワークエリアは、同じメモリ領域にとることができる。こうすると少ないメモリでプログラムを実行できるようになるし、80486以降ではキャッシュを有効に使うことになる。ただし、誤って同時に使ってしまわないように、常に気をつける必要がある。
<H3><A NAME="PREFIX">プリフィックス</A></H3>
プリフィックスつきの命令は、8086では2クロックの追加となる。80286と80386では、命令のデコードと実行は独立したユニットで行われ、デコードされた命令はキューに蓄えられるため、プリフィックスのデコードにかかる時間が以前の命令の実行時間に隠れてしまうので、普通は追加クロックは必要ない。
<P>プリフィックスのうち、セグメントオーバーライドプリフィックスは、プログラムの工夫でなくすことができる。多くの命令では、DSをデフォルトセグメントとして使うので、よく使うデータはなるべくDSでアクセスできるセグメントに置く。BPを含むアドレシングモードでは、デフォルトセグメントがSSになるので、DSとSSが同じセグメントを指しているとき以外は、DSの指すセグメントにあるデータをアクセスするのにBPを使ったアドレシングモードを使わないほうがよい。
<H3>セグメント</H3>
80286以前では、一つのセグメントの大きさは64Kバイトまでであり、また、セグメントレジスタは4つしかなくて役割が決まっているため、一度に扱えるデータ量は少ない。大量のデータを一度に扱うには工夫が必要である。
<P>もし、速度的に重要な場所でセグメントの大きさを越えるデータを使うときには、セグメント内の処理を行うループの外に、セグメントレジスタを設定するコードを置くようにし、セグメントレジスタを頻繁にいじるのは避けるべきである。例えば、セグメントアドレスBXからBPパラグラフをクリアするには、次のようにする。
<PRE> 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
</PRE>
速度的にそれほど重要でなければ、次のようにするのもよい。
<PRE> XOR AX,AX
L1: MOV DI,AX
MOV ES,BX
MOV CX,8
REP STOSW
INC BX
DEC BP
JNZ L1
</PRE>
<P>異なるセグメントにある何種類かのデータを同時に使いたいなら(例えば、二つまたは三つのデータに演算をして画面に書き込むような場合)、CSやSSの指すセグメントにデータを置いてもよい。逆に、データのあるセグメントの一部にコードを書いたり、スタックをとったりしてもよい。ただし、プロテクトモードでは、コードセグメントに書き込みができないので、CSを使ったデータアクセスでは読み出ししかできない。
<P>データが128Kバイトまでなら、DSとESに分ける方法がある。例えば、MS-DOSの16ビットFATのように、高々2^16個の16ビットデータを扱う場合は、16ビットデータを上位と下位に分け、下位をDSの指すセグメントに、上位をESの指すセグメントに格納することができる。こうすれば、セグメントレジスタの値を変えずに全データをアクセスできる。
<H3>値の利用</H3>
値がわかっているレジスタは利用する。例えば、ループを抜けたとき、ループカウンタに使ったレジスタの値は0であることを利用できる。また、次のループ
<PRE> XOR AX,AX
MOV [X],AX
L1: ;
; 処理
;
MOV AX,[X]
INC AX
MOV [X],AX
CMP AX,[X_LIMIT]
JNE L1
</PRE>
では、「処理」の先頭でメモリ上のデータXの値がAXに入っていることを利用できる。
<P>複数のレジスタに同じ値を入れたいときには、一つのレジスタに入れた値をMOV命令でコピーすればよい。バイトレジスタでなければ、コードが短くなる。8086ではクロック数も短縮される(バイトレジスタでも)。PentiumとPentiumProでは、依存関係が増えて並列度が下がるかもしれないので、注意する。
<P>AXのmsbが0であるとわかっているとき、例えば直前にAXを0にした場合などで、DXを0にしたいときは、CDW命令を使うとバイト数を節約できる。
<PRE> Bytes 8086 80286 80386 80486
XOR DX,DX 2 3 2 2 1
CWD 1 5 2 2 3
</PRE>
<H3>アラインメント</H3>
奇数アドレスのワードデータをアクセスするには、メモリアクセスが2回必要になり、8086と80186で最低4クロック、80286で最低2クロックの追加サイクル(メモリアクセスのウェイトによる)が必要になる。ワードデータはなるべく偶数アドレスに置く(アラインする)ようにする。
<P>80286以前では、命令のフェッチはワード単位で行われるので、よく使うジャンプ先は偶数アドレスに置いて、ジャンプ先の最初の命令全体がなるべく速くフェッチされるようにするとよい。アセンブラでは、EVEN擬似命令を使うと、その次の命令が偶数アドレスになるように、必要ならNOP命令を出力する。サブルーチンの先頭のラベルはこれで問題ないが、途中のラベルのときは、NOPの実行時間を節約するために、前の命令を故意に1バイト長くしたほうがよいこともある。例えば、次のコードでEVEN擬似命令がNOPを生成するなら、
<PRE> MOV AX,[SI+4]
EVEN
L1: MOV [DI],AX
</PRE>
代わりに
<PRE> DB 8BH,84H,04H,00H ; MOV AX,[SI+0004]
L1: MOV [DI],AX
</PRE>
と書いたほうがよい。
<H3>時間のかかる命令を減らす</H3>
除算命令は、非常に時間のかかる命令で、使えるレジスタが限定されるので、速度が重要な場所ではなるべく使わないようにする。除数が2の冪なら、シフト命令を使う。そうでない場合でも、逆数の乗算に置き換える(文献2、文献5参照)。
<P>乗算命令も、時間のかかる命令なので、速度が重要な場所では、MOVとシフトと加算の組合せに置き換える。例えば、AXを3倍するには
<PRE> MOV CX,AX
SHL AX,1
ADD AX,CX
</PRE>
とすればよいし、AXの3倍をBXに加えるには、 ADD BX,AX を3回繰り返せばよい。AXを257倍するなら MOV AH,AL である。80186以降では、80186で追加された、即値を含む3オペランドのIMUL命令(演算数と演算結果が同じビット数なので、フラグレジスタを無視すればMUL命令としても使える)を使うほうが便利なこともある。
<P>時間のかかる演算の結果は、あらかじめ計算しておいて、メモリ上に置いておいたほうがよいこともある。例えば、円を描くための三角関数の値などは、メモリ上にテーブルを作っておいたほうがよい。
<H3><A NAME="STRING">ストリング操作命令</A></H3>
80286以前では、ストリング操作命令を使うことは、プログラムを小さくし、かつ高速化する。ストリング操作命令は、プレフィクスを除けばすべて1バイト命令で、複数の処理をまとめて高速に行うからである。例として、二つのストリング操作命令、LODSB,STOSB命令と、それと同等な命令列を比較する。
<PRE> 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#
# <A HREF="#PREFIX">プリフィックス</A>のクロックを含む
</PRE>
<P>ストリング操作命令は、短い代わりに、使えるレジスタは固定されている。変えられるのは、方向(読み込みまたは書き込み後に、SIまたはDIを増やすか減らすかをDFで指定)と、転送元のセグメント(セグメントオーバーライドプリフィックスを使う)だけである。
<P>リピートプリフィックスを使って繰り返し処理ができるのも、ストリング操作命令の有利な点である。ただし、8086では、セグメントオーバーライドプリフィックスと同時に使うと、うまく動作しないことがある(途中で割り込みがかかったときの処理にミスがあるため)。
<P>REP STOS 命令や REP MOVS 命令で、たくさんのデータをストア/コピーするときには、なるべく大きいオペランドサイズを使うとよい。繰り返し回数が奇数の場合を考慮するなら、
<PRE> SHR CX,1
REP MOVSW
RCL CX,1
REP MOVSB
</PRE>
のような命令列を使う。
<H3>XCHG命令</H3>
XCHG命令は、同時に二つのオペランドに書き込みのできる、便利な命令である(80486以降では、相対的に遅いので、あまり使わないほうがよい)。次のような応用がある。
<UL>
<LI>特定のレジスタを必要とする命令を、値を変えて交互に使う。例えば、DX:AXから始まるCX個の32ビット数を、ES:[DI]から順にストアするには、
<PRE>L1: STOSW
XCHG AX,DX
STOSW
XCHG AX,DX
ADD AX,1
ADC DX,0
LOOP L1
</PRE>
とする。サブルーチン、ファンクションリクエストを呼ぶのにも使える。余分なレジスタを使う必要がないのが利点である。
<LI>レジスタの退避と値のロードを同時に行う。同様に、値のストアとレジスタの復帰を同時に行う。例えば、
<PRE> PUSH BX
MOV BX,[POINTER]
;
; 処理
;
MOV [POINTER],BX
POP BX
</PRE>
の代わりに
<PRE> XCHG BX,[POINTER]
;
; 処理
;
XCHG BX,[POINTER]
</PRE>
とする。
<PRE> Bytes 8086 80286 80386 80486
PUSH BX / MOV BX,[POINTER] 5 24 8 6 2
XCHG BX,[POINTER] 4 22 5 5 5
</PRE>
<PRE> Bytes 8086 80286 80386 80486
MOV [POINTER],BX / POP BX 5 22 8 6 2
XCHG BX,[POINTER] 4 22 5 5 5
</PRE>
<LI>2回のメモリアクセスを1命令にまとめる。例えば、メモリ上のワードデータA,B,Cを、A→B→Cとコピーしたいとき、
<PRE> MOV AX,[B]
MOV [C],AX
MOV AX,[A]
MOV [B],AX
</PRE>
とする代わりに、
<PRE> MOV AX,[A]
XCHG AX,[B]
MOV [C],AX
</PRE>
とする。
<PRE> 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
</PRE>
メモリ上のワードデータをAXに入れ、BXをそこに書き込む場合は次のようになる。
<PRE> 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
</PRE>
どちらも、8086では速くならないが、AXが他のレジスタだと速くなる。
</UL>
<P>クロック数が増えてもバイト数を減らしたいときは、AXから、またはAXへのMOV命令の代わりに、XCHG命令を使うこともある。
<UL>
<LI>BXをAXにコピーする(BXが変化してよい場合)
<PRE> Bytes 8086 80286 80386 80486
MOV AX,BX 2 2 2 2 1
XCHG AX,BX 1 3 3 3 3
</PRE>
</UL>
<H3>AAM,AAD命令</H3>
準備中
<H3>応用</H3>
準備中
<UL>
<LI>ビットはめ込み
<P>ビットマップの操作などで、用意したマスクのビットが1の桁にだけ、あるパターンをはめ込みたいことがある。例えば、
<PRE>元のビットマップ
01110111
パターン
10101010
マスク
00111100
結果
01101011
</PRE>
のような処理である。元のビットマップが[DI]に、パターンがAXに、マスクがDXにある場合、
<PRE> AND AX,DX
NOT DX
AND [DI],DX
OR [DI],AX
</PRE>
のように処理すればできるが、
<PRE> XOR AX,[DI]
AND AX,DX
XOR [DI],AX
</PRE>
のようにしたほうが速く短い。
</P>
<LI>マスクの生成
<P>上記のようにしてポリゴンなどを描画するには、与えられた範囲のマスクを生成する必要がある。
下位のCL桁が0で残りは1
<PRE> MOV AX,0FFFFH
SHL AX,CL
</PRE>
下位のCL桁が1で残りは0
<PRE> MOV AX,0001H
SHL AX,CL
DEC AX
</PRE>
下位のCL+1桁が0で残りは1
<PRE> MOV AX,0FFFEH
SHL AX,CL
</PRE>
下位のCL+1桁が1で残りは0
<PRE> MOV AX,0002H
SHL AX,CL
DEC AX
</PRE>
上位のCL桁が0で残りは1
<PRE> MOV AX,0FFFFH
SHR AX,CL
</PRE>
上位のCL桁が1で残りは0
<PRE> MOV AX,0FFFFH
SHR AX,CL
NOT AX
</PRE>
上位のCL+1桁が0で残りは1