-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathhow_to_o.htm
4359 lines (4097 loc) · 385 KB
/
how_to_o.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>How to optimize for the Pentium family of the microprocessors</TITLE>
<META content="text/html; charset=shift_jis" http-equiv=Content-Type>
<META content="MSHTML 5.00.2919.6307" name=GENERATOR>
</HEAD>
<BODY TEXT="#000000" BGCOLOR="#F0FFE0" LINK="#0000E0" VLINK="#E000E0" ALINK="#FF0000">
<H1><CENTER>How to optimize for the Pentium<BR>
family of the microprocessors (In Japanese)</H1>Original (in English): <A href="http://www.agner.org/assem/">http://www.agner.org/assem/</A><BR>Copyright (c) 1996, 2000 by Agner Fog. Last modified 2000-03-31.<BR>
Translation into Japanese by Nobuhisa Fujinami and Takashi Itoh. Last modified 2000-05-30.<BR>
</CENTER>
<P>このページは、Agner Fogさんによる同名のマニュアルの、藤波順久及び伊東尚志による日本語訳です。原文(英語)の著作権はAgner Fogさんにあります。また、日本語訳中の「私」とは、Agner Fogさんのことです。原文は<A href="http://www.agner.org/assem/">http://www.agner.org/assem/</A>を参照してください。
<P><H3><U>注意: このページは、現在誤訳の訂正を行っています。それとは無関係に、正確な内容については常に原文を参照してください。</U></H3>
<H2>目次</H2>
<OL>
<LI><A href="#1">はじめに</A>
<LI><A href="#2">文献</A>
<LI><A href="#3">高級言語からアセンブリ言語の関数を呼ぶには</A>
<LI><A href="#4">デバッグと確認</A>
<LI><A href="#5">メモリモデル</A>
<LI><A href="#6">アラインメント</A>
<LI><A href="#7">キャッシュ</A>
<LI><A href="#8">初めての実行と繰り返し実行の比較</A>
<LI><A href="#9">番地生成インターロック</A>(PPlain and PMMX)
<LI><A href="#10">整数命令のペアリング</A>
<OL>
<LI><A href="#10_1">完全なペアリング</A>
<LI><A href="#10_2">不完全なペアリング</A>
</OL>
<LI><A href="#11">複雑な命令を単純な命令に分割</A>(PPlain and PMMX)
<LI><A href="#12">プリフィクス</A>(PPlain and PMMX)
<LI><A href="#13">パイプラインの概要</A>(PPro, PII and PIII)
<LI><A href="#14">命令のデコード</A>(PPro, PII and PIII)
<LI><A href="#15">命令の取り込み</A>(PPro, PII and PIII)
<LI><A href="#16">レジスタ・リネーミング</A>(PPro, PII and PIII)
<OL>
<LI><A href="#16_1">依存関係の解消</A>
<LI><A href="#16_2">レジスタ・リード・ストール</A>
</OL>
<LI><A href="#17">アウト・オプ・オーダー実行</A>(PPro, PII and PIII)
<LI><A href="#18">リタイアメント</A>(PPro, PII and PIII)
<LI><A href="#19">パーシャル・ストール</A>(PPro, PII and PIII)
<OL>
<LI><A href="#19_1">パーシャル・レジスタ・ストール</A>
<LI><A href="#19_2">パーシャル・フラグ・ストール</A>
<LI><A href="#19_3">シフト命令・回転命令の後のフラグ・ストール</A>
<LI><A href="#19_4">パーシャル・メモリ・ストール</A>
</OL>
<LI><A href="#20">依存の連鎖</A>(PPro, PII and PIII)
<LI><A href="#21">ボトルネックを探す</A>
<LI><A href="#22">ジャンプと分岐</A>(全てのプロセッサ)
<OL>
<LI><A href="#22_1">分岐予測</A>(PPlain)
<LI><A href="#22_2">分岐予測</A>(PMMX, PPro, PII and PIII)
<LI><A href="#22_3">ジャンプの回避</A>(全てのプロセッサ)
<LI><A href="#22_4">フラグを用いた条件分岐の回避</A>(全てのプロセッサ)
<LI><A href="#22_5">条件分岐を条件移動命令で置き換える</A>(PPro, PII and PIII)
</OL>
<LI><A href="#23">コードサイズの縮小</A>(全てのプロセッサ)
<LI><A href="#24">浮動小数点コードのスケジューリング</A>(PPlain and PMMX)
<LI><A href="#25">ループの最適化</A>(全てのプロセッサ)
<OL>
<LI><A href="#25_1">ループの最適化</A>(PPlain and PMMX)
<LI><A href="#25_2">ループの最適化</A>(PPro, PII and PIII)
</OL>
<LI><A href="#26">問題となりやすい命令</A>
<OL>
<LI><A href="#26_1">XCHG命令</A>(全てのプロセッサ)
<LI><A href="#26_2">キャリーフラグを通した回転命令</A>(全てのプロセッサ)
<LI><A href="#26_3">ストリング命令</A>(全てのプロセッサ)
<LI><A href="#26_4">ビットテスト命令</A>(全てのプロセッサ)
<LI><A href="#26_5">整数乗算命令</A>(全てのプロセッサ)
<LI><A href="#26_6">WAIT命令</A>(全てのプロセッサ)
<LI><A href="#26_7">FCOM命令+FSTSW AX命令</A>(全てのプロセッサ)
<LI><A href="#26_8">FPREM命令</A>(全てのプロセッサ)
<LI><A href="#26_9">FRNDINT命令</A>(全てのプロセッサ)
<LI><A href="#26_10">FSCALE命令</A>(全てのプロセッサ)
<LI><A href="#26_11">FPTAN命令</A>(全てのプロセッサ)
<LI><A href="#26_12">FSQRT命令</A>(PIII)
<LI><A href="#26_13">MOV [MEM], ACUUM命令</A>(PPlain and PMMX)
<LI><A href="#26_14">TEST命令</A>(PPlain and PMMX)
<LI><A href="#26_15">ビットスキャン命令</A>(PPlain and PMMX)
<LI><A href="#26_16">FLDCW命令</A>(PPro, PII and PIII)
</OL>
<LI><A href="#27">特別な話題</A>
<OL>
<LI><A href="#27_1">LEA命令</A>(全てのプロセッサ)
<LI><A href="#27_2">除算命令</A>(全てのプロセッサ)
<LI><A href="#27_3">浮動小数点レジスタの解放</A>(全てのプロセッサ)
<LI><A href="#27_4">浮動小数点からMMX命令への移行</A>(PMMX, PII and PIII)
<LI><A href="#27_5">浮動小数点の整数への変換</A>(全てのプロセッサ)
<LI><A href="#27_6">整数命令を使った浮動小数点演算</A>(全てのプロセッサ)
<LI><A href="#27_7">浮動小数点命令を使った整数演算</A>(PPlain and PMMX)
<LI><A href="#27_8">データブロックの移動</A>(全てのプロセッサ)
<LI><A href="#27_9">自己改変コード</A>(全てのプロセッサ)
<LI><A href="#27_10">プロセッサの見分け方</A>(全てのプロセッサ)
</OL>
<LI><A href="#28">命令タイミング</A>(PPlain and PMMX)
<OL>
<LI><A href="#28_1">整数演算命令</A>
<LI><A href="#28_2">浮動小数点演算命令</A>
<LI><A href="#28_3">MMX命令</A>(PMMX)
</OL>
<LI><A href="#29">命令タイミングとμ-OPSへの分解</A>(PPro, PII and PIII)
<OL>
<LI><A href="#29_1">整数演算命令</A>
<LI><A href="#29_2">浮動小数点演算命令</A>
<LI><A href="#29_3">MMX命令</A>(PII and PIII)
<LI><A href="#29_4">XMM命令</A>(PIII)
</OL>
<LI><A href="#30">コードの速度のテスト</A>
<LI><A href="#31">いろいろなマイクロプロセッサの比較</A>
</OL>
<P>
<HR>
<A name=1>
<H2>1. はじめに</H2>
このマニュアルは、最適化されたアセンブリ言語のコードの書き方について、詳述する。特に、Pentium(R)ファミリ・マイクロプロセッサに焦点をあてる。
<P>このマニュアルの情報は、私自身の調査と試験に基づいており、さまざまな人たちから受け取った情報で補足されている。このマニュアルのために追加情報を私に送ってくれた人々に感謝したい。このマニュアルは他の情報源に比べて理解しやすく正確で、他に見られない細かい事項をたくさん含んでいる。この情報を使えば、あなたは多くの場合に、あるコード片が正確に何クロックサイクルかかるのか計算することができるようになる。私はこのマニュアルに含まれている情報のすべてが正しいとは主張しない。いくつかのタイミング等は正確に測定することが困難あるいは不可能で、また私には、インテルのマニュアルの著者が持っているような技術的な内部情報を見る手段がない。
<P>このマニュアルでは次の様なバージョンのPentiumプロセッサについて議論する。
<PRE>
略称 名前
------------------------------------------------
PPlain plain old Pentium (without MMX)
PMMX Pentium with MMX
PPro Pentium Pro
PII Pentium II(CeleronとXeonを含む)
PIII Pentium III(変種を含む)
------------------------------------------------
</PRE>
<P>アセンブリ言語の文法はMASM5.10に従っている。公式なX86のアセンブリ言語は存在しないが、大部分のアセンブラがMASM5.10互換モードを持っているため、あなたが手にすることのできる事実上の標準に最も近い。しかしながら、私はMASM5.10の使用をお勧めしない。というのは、32ビットモードにおいて深刻なバグがあるからである。TASMか若しくはより新しいバージョンのMASMをお勧めする。
<P>このマニュアルの中のいくつかの注釈はインテルに対する批判と映るかもしれない。しかし、他のメーカーの方が優れているという意味に取らないでいただきたい。ペンティアム・マイクロプロセッサ・ファミリーは、おそらくどの競争メーカーよりも速いと思われ、またそのように証明されており、良い試験結果がある。こういった理由により、他の競争メーカーについては、私や他のいかなる人の似たような独自の調査もされたことはない。
<P>アセンブリ言語でのプログラミングは、高級言語よりはるかに難しい。バグを生成するのは容易であり、その発見はたいへん困難である。ここで警告である!
読者は既にアセンブリ言語の経験があると仮定する。もしそうでなければ、複雑な最適化を始める前に、どうかアセンブリ言語に関する本を何か読んで、プログラミングの経験を得てほしい。
<P>PPlain、PMMXチップのハードウェア設計は、一般的な最適化方法を使ったというよりはむしろ、いくつかのよく使われる命令やその組合せに特に最適化された、多くの特徴を持っている。その結果、ソフトウェアをこの設計向きに最適化するのはかなり複雑で、多くの例外があるが、相当な性能向上が可能かもしれない。PPro、PII、PIIIプロセッサはたいそう異なった設計となっており、プロセッサは命令をアウト・オブ・オーダー実行することにより、かなりの最適化作業の面倒をみている。しかし、これらのプロセッサのより複雑な設計は多くの潜在的なボトルネックを作り出しており、これらのプロセッサ向けに手で最適化することにより多くの利益があるかもしれない。
<P>コードをアセンブリ言語に変換する前に、使っているアルゴリズムが最適であることを確認してほしい。コード片をアセンブラコードに直すよりアルゴリズムを改良したほうがずっとよい結果になることがしばしばある。
<P>次に、プログラムの決定的に重要な部分を同定しなければならない。しばしば、99%以上のCPU時間がプログラムの最も内側のループで消費されている。この場合、そのループだけを最適化し、それ以外はすべて高級言語のままにしておくべきである。アセンブラプログラマの中には、プログラムの誤った部分を最適化するのにエネルギーを浪費し、努力の主な効果が、プログラムのデバッグや保守を難しくしただけという人もいる!
<P>もしプログラムの決定的に重要な部分がどこか明らかでなければ、プロファイラを使ってみつけるとよい。もしボトルネックがディスクアクセスであるとわかったら、アセンブリプログラミングに行くのではなくて、ディスクアクセスをシーケンシャルに行うようにプログラムを変更して、ディスクのキャッシングを改良するとよい。もしボトルネックがグラフィクスの出力なら、グラフィクスの手続きを呼ぶ回数を減らす方法を探すとよい。
<P>高級言語のコンパイラのいくつかは特定のプロセッサ向けの比較的よい最適化を提供しているが、手でさらに最適化することは、普通もっと良い性能を生み出すことができる。
<P>どうか私にプログラミングの質問を送らないでほしい。私はあなたの宿題をするつもりはない。
<P>ナノ秒狩りの幸運を祈る!
<P>
<HR>
<A name=2>
<H2>2. 文献</H2>
たくさんの有用な文献が、インテルのWWWサイトから無料でダウンロード可能であり、また印刷物やCD-ROMとして得ることができる。
<P>文献のURLは頻繁に変わるので、ここで紹介しない。<A href="http://www.intel.com/sites/developer/search.htm">http://www.intel.com/sites/developer/search.htm</A>の検索機能を使うか、<A href="http://www.agner.org/assem">http://www.agner.org/assem</A>からリンクをたどることで、必要な文書を見つけることができる。
<P>いくつかの文書は.PDF形式である。.PDFファイルを見たり印刷したりするソフトウェアを持っていなければ、<A href="http://www.adobe.com/">http://www.adobe.com/</A>からAcrobatファイルリーダをダウンロードすればよい。
<P>特定のアプリケーションを最適化するための、MMX命令、XMM(SIMD)命令の使い方は、アプリケーションノートのいくつかで述べられている。これらの命令セットはいろいろなマニュアルやチュートリアルで述べられている。
<P>VTUNEはコードを最適化するためにインテルから出ているツールである。私はこのツールをテストしたことはないので、ここではいかなる評価も与えることはできない。
<P>インテル以外にも有用な情報源がたくさんある。これらはニュースグループ<A
href="news://comp.lang.asm.x86/">comp.lang.asm.x86</A>のFAQにリストされている。シェアウェアのエディタASMEDITには、すべての命令コードなどをカバーするオンラインヘルプがある。ASMEDITは<A href="http://www.inf.tu-dresden.de/~ok3/asmedit.html">http://www.inf.tu-dresden.de/~ok3/asmedit.html</A>から得られる。
<P>インタネットのリソースについては<A href="http://www.agner.org/assem/">http://www.agner.org/assem/</A>からリンクをたどってほしい。
<P>
<HR>
<A name=3>
<H2>3. 高級言語からアセンブリ言語の関数を呼ぶには</H2>
インラインアセンブラを用いる方法と、サブルーチンを完全にアセンブラで書き、あなたのプロジェクト中でリンクする方法がある。後者を選んだ場合は、高水準言語を直接アセンブリ言語に翻訳できるコンパイラの使用をお勧めする。関数コールの方法が正しく得られることが確実になる。これは大部分のC++コンパイラで可能である。
<P>関数の呼び出し方法と名前の変換はたいそう複雑である。多数の異なる呼び出し方法があり、コンパイラのブランドが異なればこの点で互換性がない。アセンブリ言語のサブルーチンをC++から呼び出そうとしているなら、整合性と互換性の点から最もよい方法は、関数を extern "C" と _cdecl で宣言することである。アセンブリ言語のコードは、アンダースコア(_)が先頭についた関数名を持ち、外部名の大文字小文字を区別する(オプション -mx)ようにアセンブルされるはずである。
<P>オーバーロード関数や、オーバーロード演算子、メンバ関数その他のC++特有の機能を使うには、まずC++でコーディングし、正しいリンク情報と呼び出し規約を得るためにC++ソースをアセンブリ言語に翻訳する必要がある。細部については、各々のメーカーのコンパイラ毎に異なる。アセンブリ関数を extern "C" と _cdecl で宣言することなしに異なったコンパイラで呼び出し可能にするには、各々のコンパイラ毎に外部参照名を与える必要がある。例えば、オーバーロードされた square 関数を次に示す。
<PRE> ; int square (int x);
SQUARE_I PROC NEAR ; 整数2乗関数
@square$qi LABEL NEAR ; Borland コンパイラのためのリンク名
?square@@YAHH@Z LABEL NEAR ; Microsoft コンパイラのためのリンク名
_square__Fi LABEL NEAR ; Gnu コンパイラのためのリンク名
PUBLIC @square$qi, ?square@@YAHH@Z, _square__Fi
MOV EAX, [ESP+4]
IMUL EAX
RET
SQUARE_I ENDP
; double square (double x);
SQUARE_D PROC NEAR ; 倍精度浮動小数点2乗関数
@square$qd LABEL NEAR ; Borland コンパイラのためのリンク名
?square@@YANN@Z LABEL NEAR ; Microsoft コンパイラのためのリンク名
_square__Fd LABEL NEAR ; Gnu コンパイラのためのリンク名
PUBLIC @square$qd, ?square@@YANN@Z, _square__Fd
FLD QWORD PTR [ESP+4]
FMUL ST(0), ST(0)
RET
SQUARE_D ENDP</PRE>
<P>パラメータの渡し方は呼び出し規約に依存する。
<PRE>
呼び出し規約 スタック上のパラメータの順序 パラメータの消去
_cdecl 最初のパラメータは下位アドレス 呼び出し側
_stdcall 最初のパラメータは下位アドレス サブルーチン
_fastcall コンパイラによる サブルーチン
_pascal 最初のパラメータは上位アドレス サブルーチン
</PRE>
<U>16ビットモードのDOSとWindows、CとC++におけるレジスタの用途</U><BR>
16ビットの戻り値はAXレジスタ、32ビットの戻り値はDX:AX、浮動小数点の戻り値はST(0)である。レジスタAX, BX, CX, DX, ESと算術フラグは手続きによって破壊されるかもしれない。すなわちすべての他のレジスタはどこかに保存し、しかる後に復元しなければならない。手続きはレジスタSI, DI, BP, DSに頼ることもあり、SSは他の手続きを呼んでも変化しない。
<P><U>32ビットモードのWindows、C++または他の言語の場合</U><BR>
整数の戻り値はEAXレジスタ、浮動小数点の戻り値はST(0)である。レジスタEAX, ECX, EDX(EBXは除く)は手続きによって破壊されるかもしれない。すなわちすべての他のレジスタはどこかに保存し、しかる後に復元しなければならない。セグメントレジスタは一時的にも破壊してはならない。フラットセグメントを指すすべてのCS, DS, ESとSSがそうである。FSはオペレーティング・システムによって使われる。GSは使われないが、予約されている。フラグは以下の制約下で変化する可能性がある。方向フラグは初期値0である。方向フラグは一時的にセットされるかもしれないが、いかなる呼び出しまたは戻る前にも、クリアされていなければならない。浮動小数点レジスタ・スタックは手続きの始まりにおいて空でなければならない。但しST(0)が戻り値として用いられる場合を除く。MMXレジスタは手続きによって破壊される可能性があり、浮動小数点レジスタを用いるかもしれないすべての手続きから戻る前と呼び出す前に、EMMSによって同様にクリアされるかもしれない。XMMレジスタ・パラメータの渡し方と戻り値はインテルのアプリケーションノートの589ページに書かれている。手続きはレジスタEBX, ESI, EDI, EBPに頼ることもあり、すべてのセグメントレジスタは他の手続きを呼んでも変化しない。
<P>
<HR>
<A name=4>
<H2>4. デバッグと確認</H2>
アセンブリコードをデバッグするのは、あなたがすでに気づいているかもしれないように、たいそう困難でいらいらする。私は次のようにすることを勧めたい。まず最適化したいコード片を高級言語のサブルーチンとして書くことから始め、次に、そのサブルーチンをすっかりテストするテストプログラムを書く。テストプログラムはすべての分岐や特別な場合を必ず通るようにする。
<P>高級言語で書かれたテストプログラムのサブルーチンが動くようになったら、アセンブリ言語に翻訳する準備ができたことになる。
<P>これで最適化を始めることができる。変更をするたびにコードをテストプログラム上
で走らせて、正しく動くか見るべきである。
<P>すべてのバージョンに番号をつけて保存せよ。そうすれば、テストプログラムではつかまらなかった(間違った番地に書き込んでしまうような)エラーを見つけた場合に戻ってテストし直せる。
<P>プログラムの最も決定的な部分の速度を<A href="#30">30章</A>で述べる方法でテストせよ。コードが期待に比べてはっきり遅ければ、最もありそうな理由は、キャッシュミス(<A href="#6">6章</A>)、オペランドのミスアラインメント(<A href="#5">5章</A>)、最初の実行のペナルティ(<A href="#8">8章</A>)、分岐予測ミス(<A href="#22">22章</A>)、命令取り込みミス(<A href="#15">15章</A>)、レジスタ・リード・ストール(<A href="#16">16章</A>)、または長い依存の連鎖(<A href="#20">20章</A>)である。
<P>高度に最適化されたコードは他人にとって非常に読みにくく、理解しにくくなりがちであり、たとえあなたであってもしばらく後に読み返せば同じことである。コードの維持ができるように、手続きやマクロのような小さな論理ユニットに分割することは重要である。これらはよく定義されたインターフェースと適当な注釈が必要である。コードが読みにくくなればなるほど、良い資料がより重要になる。
<P>
<HR>
<A name=5>
<H2>5. メモリモデル</H2>
Pentiumは32ビットコード向けを第一に設計されており、16ビットコードでの性能は劣る。コードとデータをセグメント分けすることも性能をはっきり劣化させるので、あなたは32ビットフラットモードを選ぶべきであり、このモードをサポートするオペレーティングシステムを選ぶべきである。このマニュアルにでてくるコードの例は、特に指定がなければ32ビットフラットメモリモデルを仮定している。
<P>
<HR>
<A name=6>
<H2>6. アラインメント</H2>
RAM上のすべてのデータは下のように2、4、8、または16で割り切れる番地にアラインするべきである。
<PRE>
アラインメント アラインメント
オペランドサイズ PPlainとPMMX PPro、PIIとPIII
------------------------------------------------------
1 (byte) 1 1
2 (word) 2 2
4 (dword) 4 4
6 (fword) 4 8
8 (qword) 8 8
10 (tbyte) 8 16
16 (oword) 利用不可 16
------------------------------------------------------
</PRE>
<P>PPlainとPMMAXにおいては、ミスアラインされたデータのアクセスは最低3クロックサイクル余計にかかる。キャッシュラインの境界をまたぐと、ペナルティはさらに高くなる。
<P>PPro, PIIとPIIIにおいては、ミスアラインされたデータがキャッシュライン境界をまたぐ時、6-12クロック余計にかかる。16バイトよりも小さいミスアラインされたオペランドでも32バイト境界をまたがなければ、ペナルティはない。
<P>8または16バイトでアラインされたDWORDのスタックは、問題となることがある。よくある方法は、アラインされたフレームポインタを用意することである。アラインされたローカルデータを持つ関数は次のようなものであろう。
<PRE>
_FuncWithAlign PROC NEAR
PUSH EBP ; 始まりのコード
MOV EBP, ESP
AND EBP, -8 ; フレームポインタを8バイトでアラインする
FLD DWORD PTR [ESP+8] ; 関数パラメータ
SUB ESP, LocalSpace + 4 ; ローカル領域の確保
FSTP QWORD PTR [EBP-LocalSpace] ; アラインされた領域に何か書き込む
...
ADD ESP, LocalSpace + 4 ; 終わりのコード ESPの復元
POP EBP ; (PPlain/PMMXでAGIストールが起きる)
RET
_FuncWithAlign ENDP
</PRE>
<P>アラインされたデータはいつも重要とは言え、PPlainとPMMXではコードのアラインは不要である。PPro、PII、PIIIにおいて、コードをアラインする原理は、(<A href="#15">15章</A>)で説明している。
<P>
<HR>
<A name=7>
<H2>7. キャッシュ</H2>
PPlainとPProはコード用に8KB、データ用に8KBのオンチップキャッシュ(一次キャッシュ)を持っている。PMMX、PIIとPIIIはコード用に16KB、データ用に16KB持っている。一次キャッシュにあるデータはちょうど1クロックサイクルで読み書きできる。一方、キャッシュミスするとたくさんのクロックサイクルを消費する。だから、キャッシュを最も有効に使うためにそれがどう働くか理解することは重要である。
<P>データキャッシュはそれぞれ32バイトのライン256個または512個から成る。キャッシュされていないデータ項目を読むたびに、プロセッサはキャッシュライン全体をメモリから読む。キャッシュラインは常に32で割り切れる物理番地にアラインされている。32で割り切れる番地から1バイト読んでしまえば、続く31バイトはほとんど追加コストなしで読み書きできる。互いに近く使われるデータ項目を32バイトのアラインされたメモリブロックにまとめることで、この利点を活用できる。もし、例えば二つの配列をアクセスするループがあるなら、二つの配列をインターリーブして一つの配列にすればよい。そうすると、いっしょに使われるデータをいっしょに格納できる。
<P>もし配列や他のデータ構造のサイズが32バイトの倍数なら、なるべく32でアラインするべきである。
<P>キャッシュはset-associativeである。その意味は、キャッシュラインには、任意のメモリ番地を割り当てられるわけではないということである。各キャッシュラインには7ビットのセット値があって、物理RAM番地のビット5から11とマッチする(ビット0~4はキャッシュラインの32バイトを定義する)。PPlainとPProは、128セット値のそれぞれについて二つのキャッシュラインを持つため、どんなRAM番地も割り当てられる可能性のあるキャッシュラインは二つである。PMMX、PIIとPIIIは四つある。
<P>この結果、キャッシュは、番地のビット5~11の値が同じなら、たかだか二つまたは四つの異なるデータブロックしか保持できない。二つの番地が同じセット値を持つかどうかは次の方法で決められる。各番地の下5ビットを0にし、32で割り切れる値を得よ。二つの切り捨てた番地の差が4096(=1000H)の倍数なら、二つの番地は同じセット値を持つ。
<P>このことを次のコード片を使って例示しよう。ここでESIは32で割り切れる番地を保持しているとする。
<PRE>
AGAIN: MOV EAX, [ESI]
MOV EBX, [ESI + 13*4096 + 4]
MOV ECX, [ESI + 20*4096 + 28]
DEC EDX
JNZ AGAIN
</PRE>
<P>ここで使われている三つの番地は、切り捨てた番地の差が4096の倍数なので、同じセット値を持つ。このループはPPlainとPProではたいへん悲惨なふるまいをする。ECXを読むとき、適当なセット値を持つ空きキャッシュラインがないので、プロセッサは二つのキャッシュラインのうち最近使われてないほう(EAXのために使われたもの)を採用し、そのキャッシュラインを [ESI + 20*4096] から [ESI + 20*4096 + 31] までのデータで満たしてECXを読む。次に、EAXを読むとき、EAXのための値を保持していたキャッシュラインは今は破棄されていることに気づく。それで、最も近くに使われたのでないキャッシュラインを採用し、それはEBXの値を保持しているものである。以下同様である。これではキャッシュミスしか起きず、ループは60クロックサイクルとかかかる。もし第3行を変更して
<PRE>
MOV ECX, [ESI + 20*4096 + 32]
</PRE>
<P>とすれば、32バイト境界を越えたので、最初の2行と同じセット値ではなくなり、3つの番地のそれぞれに問題なくキャッシュラインを割り当てられる。ループは今は3クロックサイクルしかかからない(初回を除いて)。たいへん考慮に値する改良である!
<P>既に述べたように、PMMX, PIIとPIIIは同じセット値を持つ四つのキャッシュラインを持てるように、四ウェイのキャッシュを備えている(あるインテルの説明書は誤ってPIIのキャッシュは二ウェイだと述べている)。
<P>データの番地が同じキャッシュ値かどうか決めるのは、特に異なるセグメントに散らばっているときは、たいへん難しいかもしれない。この種の問題を避ける最もよい方法は、プログラムの決定的に重要な部分で使われるすべてのデータをキャッシュより大きくない一つの連続したブロックか、キャッシュのサイズの半分以下の二つのブロック(例えば静的データで一つのブロック、スタック上のデータで一つのブロック)に入れることである。これでキャッシュラインはきっと最適に使われるようになるだろう。
<P>コードの決定的に重要な部分が大きなデータ構造やランダムなデータ番地をアクセスするなら、すべてのよく使う変数(カウンタ、ポインタ、制御変数など)を一つの連続した4kバイト以下のブロックに入れて、ランダムなデータをアクセスするための、キャッシュラインの完全なセットがあるようにしたいかもしれない。たぶんサブルーチンの引数や戻り番地のためのスタックスペースは結局必要なので、最もよいのは、よく使う静的データをスタック上の動的変数にコピーし、変更があったものは決定的に重要なループの外でコピーし戻すことである。
<P>一次キャッシュにないデータ項目を読むことは、キャッシュライン全体を二次キャッシュから満たすことになる。これはだいたい200ns(つまり100MHzシステムでは20クロック、200MHzシステムでは40クロック)かかるが、最初に読みたかったバイトは50~100nsで利用可能になる。データ項目が二次キャッシュにもない場合、200~300nsの遅れが生ずる。DRAMのページ境界をまたぐと、この遅れは多少長くなる(DRAMのページサイズは、4または8MBの72ピンRAMモジュールで1KB、16または32MBモジュールで2KBである)。
<P>メモリから大きなデータブロックを読む時、その速度はキャッシュラインを満たす時間によって制限を受ける。データを非連続的に読むことにより、速度を改善することができる場合がある。すなわち、一つのキャッシュラインからデータを読み終える前に、次のキャッシャラインの最初の要素を読み始める場合である。この方法はPPlainとPMMXではメモリと二次キャッシュから読む場合、PPro、PIIとPIIIでは二次キャッシュから読む場合に、20%~40%速度を上げることができる。この方法の欠点はもちろん、プログラムが汚く複雑になることである。この技巧についての更なる情報は<A href="http://www.intelligentfirm.com" target="external">www.intelligentfirm.com</a>をご覧いただきたい。
<P>一次キャッシュにない番地に書いたときには、PPlainとPMMXでは、その値はそのまま二次キャッシュかRAMに行く(二次キャッシュがどう設定されているかによる)。これはだいたい100nsかかる。もし8回かそれ以上同じ32バイトメモリブロックに書き、そこから読むことがなく、ブロックが一次キャッシュにないならば、そのブロックから最初にダミーの読み込みをしてキャッシュラインにロードするほうが有利かもしれない。同じブロックへの引き続く書き込みはすべて、キャッシュに行き、それは1クロックサイクルしかかからない。これは、書き込みミスで常にキャッシュラインをロードする、PProやPIIでは必要ない。PPlainとPMMXでは、同じ番地に繰り返し書き、その間に読まないと、ときどき小さなペナルティがある。
<P>PPro、PIIとPIIIでは、書き込みミスでは通常、キャッシュラインをロードする。しかし、メモリの領域が違う振る舞いをするように設定しておくこともできる。例えば、VRAMのように。(Pentium Pro ファミリ ディベロッパーズマニュアル 下巻 オペレーティング・システム ライターズマニュアルを参照のこと。)
<P>メモリーの読み書きの速度を上げるこれ以外の方法については
<A href="#27_8">27章8</A>で後述する。
<P>PPlainとPProは二つの書き込みバッファを持っており、PMMX、PIIとPIIIは四つである。PMMX、PIIとPIIIでは、キャッシュされていないメモリに対して最大四つまでの未完了の書き込みがあっても、引き続く命令を遅らせることはない。各々の書き込みバッファは64ビットまでのオペランドを扱うことができる。
<P>スタック領域はキャッシュにあることがたいへん多いので、一時データはスタックに格納すると便利である。しかしながらDWORDサイズのスタックにQWORDデータを格納したり、WORDサイズのスタックにDWORDデータを格納する場合は、アラインメントの問題の可能性があることを認識するべきである。
<P>もし二つのデータ構造の寿命の範囲が重ならない場合、キャッシュの効率を上げるために同じRAM領域を使うかもしれない。これは一時変数をスタックに割り付けるという日常習慣と整合性がある。
<P>一時データをレジスタに格納することはもちろんもっと効率的である。レジスタは希少なリソースなので、スタックのデータをアクセスするのに[EBP]ではなく[ESP]を使い、EBPを他の目的のために空けたいかもしれない。ESPの値はPUSHやPOPをするたびに変化することを忘れないでほしい(16ビットWindowsでは、ESPを使うことはできない。タイマ割り込みがコード中の予測できない場所でESPの上位ワードを変更する)。
<P>コード用には別のキャッシュがあり、それはデータキャッシュと似ている。コードキャッシュのサイズは、PPlainとPProで8KB、PMMX、PIIとPIIIで16KBである。コードの決定的に重要な部分(最も内側のループ)がキャッシュに収まることは重要である。よく使われるコード片やいっしょに使われるルーチンはなるべく互いに近くに格納するべきである。めったに使われない分岐や手続きはコードの下のほうかどこか別の場所に離しておくべきである。
<P>
<HR>
<A name=8>
<H2>8. 初めての実行と繰り返し実行の比較</H2>
初めて実行されるコード片は、普通繰り返し実行されるよりも多くの時間がかかる。その理由は次の通りである。
<OL><LI>RAMからキャッシュへコードを読み込む時間は、実行時間よりも長い。
<LI>実行コードにより参照されるデータはキャッシュへ読み込まれなければならず、それには命令を実行するよりもはるかに多くの時間がかかるかもしれない。コードが繰り返し実行されれば、より似たようなデータがキャッシュに入る。
<LI>ジャンプ命令は初めての実行の際にはBTBにないので、分岐予測はほとんど役に立たない。<A
href="#22">22章</A>をご覧いただきたい。
<LI>PPlainにおいては、コードの解釈がボトルネックである。命令長を決定するのに1クロックサイクルを要すると、クロックサイクル当たり2つの命令を解釈することは不可能である。と言うのは、プロセッサは次の命令がどこから始まるのかがわからないからである。PPlainはこの問題をキャッシュに残っている命令が実行されてから、その長さを記憶しておくことにより解決している。この結果として、命令の組は最初の実行時は、二つの命令の前者が1バイト長である場合を除き、ペアになって実行されない。PMMX, PPro, PIIとPIIIは最初の解釈時にこのペナルティを受けない。
</OL>
<P>これら四つの理由により、ループ中にあるコード片は一般的に、最初の実行時には、続く実行時より余計な時間がかかる。
<P>もしコードキャッシュに収まりきらない大きなループがあると、キャッシュから実行されないため、毎回ペナルティを受ける。それゆえ、ループをキャッシュ内に収めるように試みるべきである。
<P>ループ中に多くのジャンプ、呼び出し、分岐がある場合、BTBミスのペナルティが繰り返し起きる。
<P>同様に、ループがデータキャッシュには大きすぎるデータ構造に繰り返しアクセスすれば、毎回データキャッシュミスのペナルティを受ける。
<P>
<HR>
<A name=9>
<H2>9. 番地生成インターロック(AGI) (PPlain and PMMX)</H2>
メモリをアクセスする命令で必要な番地の計算には1クロックサイクルかかる。普通はこの計算は、先立つ命令や命令ペアが実行されている間に、パイプラインの別のステージで行われる。しかしもし、番地が一つ前のクロックサイクルで実行された命令の結果に依存する場合は、番地の計算のために1クロックサイクル余分に待たなければならない。これはAGIストールと呼ばれる。
例:
<PRE>
ADD EBX,4 / MOV EAX,[EBX] ; AGIストール
</PRE>
<P>この例のストールは ADD EBX,4 と MOV EAX,[EBX] の間に何か他の命令をはさむか、コードを次のように書き換えることで取り除ける。
<PRE>
MOV EAX,[EBX+4] / ADD EBX,4
</PRE>
<P>ESPを暗黙に番地指定に使う、PUSH、POP、CALL、RETのような命令でも、MOV、ADD、SUBのような命令で先立つクロックサイクル中にESPが変更された場合は、AGIストールが発生する。PPlainとPMMXはスタック操作の後のESPの値を予想する特別な回路を持つため、PUSH、POP、CALLでESPを変更した後のAGIによる遅れはない。RETの後のAGIストールは、ESPに足す即値を持つ場合に限ってある。 例:
<PRE>
ADD ESP,4 / POP ESI ; AGIストール
POP EAX / POP ESI ; ストールなし、ペア
MOV ESP,EBP / RET ; AGIストール
CALL L1 / L1: MOV EAX,[ESP+8] ; ストールなし
RET / POP EAX ; ストールなし
RET 8 / POP EAX ; AGIストール
</PRE>
<P>LEA命令も、先立つクロックサイクルで変更された、ベースまたはインデックスレジスタを使う場合、AGIストールを受ける。 例:
<PRE>
INC ESI / LEA EAX,[EBX+4*ESI] ; AGIストール
</PRE>
<P>PPro、PIIとPIIIには、メモリー読み出しとLEA命令にはAGIストールはないが、メモリ書き込みにはAGIストールが存在する。 これは、連続したコードが書き込みの終了を待つのでない限り、さほど問題にならない。
<P>
<HR>
<A name=10>
<H2>10. 整数命令のペアリング(PPlain and PMMX)</H2>
<A name=10_1>
<H3>10.1 完全なペアリング</H3>
PPlainとPMMXは、命令の実行のための二つのパイプライン、UパイプとVパイプを持つ。ある条件の元で、二つの命令を同時に、一つはUパイプで、もう一つはVパイプで実行できる。これはほとんど実行速度を倍にする。そのため、命令を並べ変えてペアにするのは有益である。
次の命令はどちらのパイプでもペアにできる。
<UL>
<LI>MOV レジスタ、メモリ、または即値を、レジスタ、またはメモリへ
<LI>PUSH レジスタ、または即値、POP レジスタ
<LI>LEA, NOP
<LI>INC, DEC, ADD, SUB, CMP, AND, OR, XOR
<LI>TESTのいくつかの形式(<A href="#26_14">26章14</A>を参照)
</UL>
次の命令はUパイプでのみペアにできる。
<UL>
<LI>ADC, SBB
<LI>SHR, SAR, SHL, SAL 回数は即値
<LI>ROR, ROL, RCR, RCL 回数は即値の1
</UL>
次の命令はどちらのパイプでも実行できるが、ペアにできるのはVパイプの時だけである。
<UL>
<LI>nearコール
<LI>shortまたはnearジャンプ
<LI>shortまたはnear条件ジャンプ
</UL>
他のすべての整数命令はUパイプでのみ実行可能であり、ペアにできない。
<P>連続する二つの命令は次の条件が満たされたときペアにできる。
<P><U>1.</U> 最初の命令はUパイプでペアにでき、二番目の命令はVパイプでペアにできる。
<P><U>2.</U> 二番目の命令は最初の命令が書くレジスタを読み書きしない。<BR>
例:
<PRE>
MOV EAX, EBX / MOV ECX, EAX ; 書き込み後読み込み、ペアにできない
MOV EAX, 1 / MOV EAX, 2 ; 書き込み後書き込み、ペアにできない
MOV EBX, EAX / MOV EAX, 2 ; 読み込み後書き込み、ペアOK
MOV EBX, EAX / MOV ECX, EAX ; 読み込み後読み込み、ペアOK
MOV EBX, EAX / INC EAX ; 読み込み後読み書き、ペアOK
</PRE>
<P><U>3.</U> 規則2でパーシャル・レジスタはレジスタ全体として扱われる。<BR>
例:
<PRE>
MOV AL, BL / MOV AH, 0 ; 同じレジスタの異なる部分への書き込み
; ペアにできない
</PRE>
<P><U>4.</U> 規則2と3にかかわらず、フラグレジスタの一部に書き込む二つの命令はペアにできる。例:
<PRE>
SHR EAX,4 / INC EBX ; ペアOK
</PRE>
<P><U>5.</U> 規則2にかかわらず、フラグに書き込む命令と条件分岐はペアにできる。例:
<PRE>
CMP EAX, 2 / JA LabelBigger ; ペアOK
</PRE>
<P><U>6.</U> 次の命令の組合せは、両方がスタックポインタを変更するという事実にもかかわらず、ペアにできる。
<PRE>
PUSH + PUSH, PUSH + CALL, POP + POP
</PRE><P>
<A name=10_7>
<U>7.</U> プリフィックスつきの命令のペアリングには制限がある。プリフィックスにはいくつかの種類がある。
<UL>
<LI>デフォルトでないセグメントを番地指定する命令は、セグメントプリフィックスを持つ。
<LI>32ビットモードで16ビットデータを使ったり、16ビットモードで32ビットデータを使ったりする命令は、オペランドサイズプリフィックスを持つ。
<LI>16ビットモードで32ビットのベースまたはインデックスレジスタを使う命令は、アドレスサイズプリフィックスを持つ。
<LI>繰り返しストリング命令は、リピートプリフィックスを持つ。
<LI>ロックされる命令は、ロックプリフィックスを持つ。
<LI>8086プロセッサに実装されていなかった命令の多くは、2バイトのオペコードを持ち、その最初のバイトは0FHである。0FHのバイトは、PPlainではプリフィックスとしてふるまうが、他の版ではそうではない。0FHプリフィックスを持つ主な命令は、MOVZX, MOVSX, PUSH FS, POP FS, PUSH GS, POP GS, LFS, LGS, LSS, SETcc, BT, BTC, BTR, BTS, BSF, BSR, SHLD, SHRD, それから、2オペランドまたは即値でないオペランドを持つIMULである。
</UL>
<P>PPlainでは、プリフィックスつき命令は、near条件分岐を除いてUパイプでのみ実行可能である。
<P>PMMXでは、オペランドサイズ、アドレスサイズ、0FHのプリフィックスつき命令は、どちらのパイプでも実行可能であるが、一方、セグメント、リピート、ロックプリフィックスつき命令はUパイプでしか実行できない。
<P><U>8.</U> 変位と即値の両方を持つ命令は、PPlainではペアにできず、PMMXではUパイプでのみ実行可能である。
<PRE>
MOV DWORD PTR DS:[1000], 0 ; ペアにできないかUパイプのみ
CMP BYTE PTR [EBX+8], 1 ; ペアにできないかUパイプのみ
CMP BYTE PTR [EBX], 1 ; ペアにできる
CMP BYTE PTR [EBX+8], AL ; ペアにできる
</PRE>
(PMMXにおける、変位と即値の両方を持つ命令の別の問題は、そのような命令は7バイトより長くなるかもしれないことで、それは、<A href="#12">12章</A>で説明するように、1クロックサイクルで1命令しかデコードできないことを意味する。)
<P><U>9.</U> 両方の命令があらかじめロードされ、デコードされている。これは<A href="#8">8章</A>で説明されている。
<P><U>10.</U>PMMXのMMX命令には特別なペアリング規則がある。
<UL>
<LI>MMXのシフト、パック、アンパック命令はどちらのパイプでも実行できるが、他のMMXシフト、パック、アンパック命令とペアにできない。
<LI>MMX乗算命令はどちらのパイプでも実行できるが、他のMMX乗算命令とペアにできない。MMX乗算命令は3クロックサイクルかかり、後ろの2クロックサイクルは、浮動小数点命令と同様に、引き続く命令とオーバーラップできる(<A href="#24">24章</A>参照)。
<LI>メモリや整数レジスタをアクセスするMMX命令はUパイプでのみ実行でき、MMXでない命令とペアにできない。
</UL>
<P>
<A name=10_2>
<H3>10.2 不完全なペアリング</H3>
ペアの二つの命令が同時に実行されなかったり、時間的に一部だけオーバーラップしたりする状況がある。しかし、最初の命令がUパイプで、二番目の命令がVパイプで実行されるので、これも依然としてペアとして考慮するべきである。不完全なペアの両方の命令の実行が完了しないと、引き続く命令の実行は始まらない。
<P>不完全なペアリングは次のような場合に起きる。
<P><U>1.</U> 二番目の命令がAGIストールを受ける場合(<A href="#9">9章</A>参照)。
<P><U>2.</U> 二つの命令はメモリの同じDWORDを同時にアクセスできない。次の例はESIが4で割り切れると仮定している。<BR>
<PRE>
MOV AL, [ESI] / MOV BL, [ESI+1]
</PRE>
二つのオペランドは同じDWORD内にあるので、同時には実行できない。このペアは2クロックサイクルかかる。<BR>
<PRE>
MOV AL, [ESI+3] / MOV BL, [ESI+4]
</PRE>
ここでは二つのオペランドはDWORD境界の両側にあるので、完全にペアになれ、1クロックサイクルしかかからない。
<P><U>3.</U> 規則2は二つの番地のビット2~4が同じである場合に拡張される(キャッシュバンク競合)。DWORDの番地に対しては、これは二つの番地の差が32で割り切れてはならないことを意味する。
例:
<PRE>
MOV [ESI], EAX / MOV [ESI+32000], EBX ; 不完全なペアリング
MOV [ESI], EAX / MOV [ESI+32004], EBX ; 完全なペアリング
</PRE>
<P>ペアにできる整数命令で、メモリにアクセスしないものは、予測ミスしたジャンプを除いて、実行に1クロックサイクルかかる。メモリから、またはメモリへのMOV命令は、データ領域がキャッシュにあって適当にアラインされていれば、やはり1クロックサイクルしかかからない。スケールされたインデックスレジスタのような複雑な番地指定モードの使用に速度のペナルティはない。
<P>ペアにできる整数命令で、メモリから読み、何らかの計算をし、結果をレジスタやフラグに格納するものは、2クロックサイクルかかる(read/modify命令)。
<P>ペアにできる整数命令で、メモリから読み、何らかの計算をし、結果をメモリに書き戻すものは、3クロックサイクルかかる(read/modify/write命令)。
<P><U>4.</U> もし、read/modify/write命令がread/modify命令またはread/modify/write命令とペアになると、それは不完全なペアリングである。
<P>消費するクロックサイクル数は次の表のようになる。
<PRE>
| 二番目の命令
| MOV or read/ read/modify/
最初の命令 | register only modify write
----------------------|----------------------------------------------
MOV or register only | 1 2 3
read/modify | 2 2 3
read/modify/write | 3 4 5
----------------------|-----------------------------------------------
</PRE>
例:
<PRE>
ADD [mem1], EAX / ADD EBX, [mem2] ; 4クロックサイクル
ADD EBX, [mem2] / ADD [mem1], EAX ; 3クロックサイクル
</PRE>
<P><U>5.</U> ペアになった二つの命令が両方とも、キャッシュミス、ミスアラインメント、または分岐予測ミスによって余分な時間がかかるとき、そのペアは各命令単独よりは時間がかかるが、二つの和よりは少ない。
<P><U>6.</U> ペアにできる浮動小数点命令にFXCH命令が続いているものは、その次の命令が浮動小数点命令でなければ、不完全なペアリングとなる。
<P>不完全なペアリングを避けるためには、どの命令がUパイプに、どの命令がVパイプに行くかを知らなければならない。これは、次のようにすればわかる。コードを逆方向に見て行って、ペアにできない、一方のパイプでしかペアにできない、または上に述べた規則のどれかのためにペアにできない命令をさがせばよい。
<P>不完全なペアリングはたいてい、命令の順序を変更することで避けられる。
例:
<PRE>
L1: MOV EAX,[ESI]
MOV EBX,[ESI]
INC ECX
</PRE>
<P>ここで二つのMOV命令は同じメモリ位置をアクセスするので、不完全なペアを形成する。この命令列は3クロックサイクルかかる。命令の順番を変えて、 INC ECX がMOV命令のどちらかとペアになるようにすれば、改良できる。
<PRE>
L2: MOV EAX,OFFSET A
XOR EBX,EBX
INC EBX
MOV ECX,[EAX]
JMP L1
</PRE>
<P>ペア INC EBX / MOV ECX,[EAX] は、後者の命令にAGIストールがあるため、不完全である。この命令列は4クロックかかる。NOPまたは他の命令を挿入して、 MOV ECX,[EAX] が代わりに
JMP L1 とペアになるようにすれば、命令列は3クロックしかかからない。
<P>
<A name=IMPERFECTPUSH>
次の例は16ビットモードで、SPが4で割り切れると仮定する。
<PRE>
L3: PUSH AX
PUSH BX
PUSH CX
PUSH DX
CALL FUNC
</PRE>
<P>ここでPUSH命令は二つの不完全なペアを形成する。なぜなら、各ペアの両方のオペランドがメモリの同じDWORDに行くからである。 PUSH BX は PUSH CX と完全なペアになれたかもしれない(DWORD境界の両側に行くから)のに、すでに PUSH AX とペアになってしまっているので、そうはならない。命令列は、従って、5クロックサイクルかかる。もしNOPか他の命令を挿入して、PUSH BX が PUSH CX と、PUSH DX が CALL FUNC とペアになるようにすれば、命令列は3クロックしかかからない。問題を解決する別の方法は、SPが必ず4で割り切れないようにすることである。16ビットモードでSPが4で割り切れるかどうか知るのは困難なので、この問題を避ける最もよい方法は、32ビットモードを使うことである。
<P>
<HR>
<A name=11>
<H2>11. 複雑な命令を単純な命令に分割(PPlain and PMMX)</H2>
read/modifyまたはread/modify/write命令を分割して、ペアリングを改良してもよい。
<P>
例:
<PRE>
ADD [mem1],EAX / ADD [mem2],EBX ; 5クロックサイクル
</PRE>
このコードは3クロックサイクルしかかからない命令列に分割できる。
<PRE>
MOV ECX,[mem1] / MOV EDX,[mem2]
ADD ECX,EAX / ADD EDX,EBX
MOV [mem1],ECX / MOV [mem2],EDX
</PRE>
<P>同様に、ペアにできない命令を、ペアにできる命令に分割してもよい。
<PRE>
PUSH [mem1]
PUSH [mem2] ; ペアにできない
</PRE>
これを分割して
<PRE>
MOV EAX,[mem1]
MOV EBX,[mem2]
PUSH EAX
PUSH EBX ; すべてペアになる
</PRE>
<P>ペアにできない命令で、より単純なペアにできる命令に分割できる、他の例:
<BR><PRE>
CDQ を分割して MOV EDX,EAX / SAR EDX,31
NOT EAX の代わりに XOR EAX,-1
NEG EAX を分割して XOR EAX,-1 / INC EAX
MOVZX EAX,BYTE PTR [mem] を分割して XOR EAX,EAX / MOV AL,BYTE PTR [mem]
JECXZ を分割して TEST ECX,ECX / JZ
LOOP を分割して DEC ECX / JNZ
XLAT の代わりに MOV AL,[EBX+EAX]
</PRE>
<P>もし命令を分割することで速度が改良されなければ、コードサイズを縮小するために、複雑な、またはペアにできない命令をそのままにしてもよい。命令の分割は、PPro、PIIとPIIIでは、分割された命令がより小さなμ-OPSを生成しない限り必要ない。
<P>
<HR>
<A name=12>
<H2>12. プリフィックス(PPlain and PMMX)</H2>
一つまたは複数のプリフィックスを持つ命令は、Vパイプで実行できないかもしれず(<A
href="#10_7">10章7</A>参照)、デコードに2クロック以上かかるかもしれない。
<P>PPlainでは、near条件ジャンプの0Fhプリフィックスを除いて、デコードの遅れは各プリフィックスあたり1クロックサイクルである。
<P>PMMXでは、0Fhプリフィックスについてのデコードの遅れはない。セグメントとリピートプリフィックスはデコードに1クロック余計にかかる。アドレスとオペランドサイズプリフィックスはデコードに2クロック余計にかかる。最初の命令がセグメントかリピートプリフィックスを持っているか、プリフィックスを持たず、二番目の命令がプリフィックスを持たないなら、PMMXはクロックサイクルあたり2命令デコードできる。アドレスまたはオペランドプリフィックスを持つ命令は、PMMXでは単独でしかデコードできない。二つ以上のプリフィックスを持つ命令は各プリフィックスについて1クロック余計にかかる。
<P>アドレスサイズプリフィックスは32ビットモードを使うことで避けられる。セグメントプリフィックスは、32ビットモードでは、フラットメモリモデルを使うことで避けられる。オペランドサイズプリフィックスは、32ビットモードでは、8ビットと32ビットの整数だけを使うことで避けられる。
<P>プリフィックスが避けられない場所では、先行する命令が実行に2クロック以上かかるなら、デコードの遅れはマスクされるかもしれない。PPlainのための規則は次の通りである。実行(デコードではない)にNクロックサイクルかかる任意の命令は、次の二つ(ときには三つ)の命令または命令ペアのN-1個のプリフィックスのデコードの遅れに「影を落とす」ことができる。言い換えれば、命令の実行にかかる余分なクロックは、それぞれ後の命令のプリフィックス一つをデコードするのに使えるということである。この影落とし効果は予測できた分岐をも越えて拡張される。2クロックサイクル以上かかる命令、AGIストール、キャッシュミス、ミスアラインメント、そのほか、デコードの遅れや分岐予測ミスを除くどんな理由によってでも遅れる命令は何でも、影落とし効果を持つ。
<P>PMMXは、同様の影落とし効果をもつが、その機構は異なる。デコードされた命令は透過な first-in-first-out (FIFO) バッファに格納され、バッファは4つまでの命令を保持できる。FIFOバッファに命令がある限り、遅れはない。バッファが空のときは、命令はデコードされるとすぐに実行される。命令が実行されるよりデコードされるのが速いとき、つまり、ペアにならない、または複数サイクルの命令があるときに、バッファは満たされる。命令がデコードされるより実行されるのが速いとき、つまり、プリフィックスによるデコードの遅れがあるとき、FIFOバッファは空になる。予測ミスした分岐の後は、FIFOバッファは空である。二番目の命令はプリフィックスなしで、どちらの命令も7バイトより長くないという前提で、FIFOバッファはクロックサイクルあたり2命令を受け取れる。二つの実行パイプライン(UとV)は、クロックサイクルあたりそれぞれFIFOバッファから1命令を受け取れる。
例:
<PRE>
CLD / REP MOVSD
</PRE>
<P>CLD命令は2クロックサイクルかかり、従ってREPプリフィックスのデコードの遅れに影を落とす。もしCLD命令が REP MOVSD から遠くにあったとしたら、コードはもう1クロックサイクルかかっていただろう。
<PRE>
CMP DWORD PTR [EBX],0 / MOV EAX,0 / SETNZ AL
</PRE>
<P>CMP命令はここではread/modify命令なので、2クロックサイクルかかる。SETNZ命令の0FhプリフィックスはCMP命令の第2クロックサイクルの間にデコードされるので、PPlainではデコードの遅れは隠される(PMMXは0FHのデコードの遅れはない)。
<P>PPro、PIIとPIIIのプリフィックスのペナルティは<A href="#14">14章</A>に述べてある。
<P>
<HR>
<A name=13>
<H2>13. PPro, PIIとPIIIのパイプラインの概要</H2>
PPro, PIIとPIIIマイクロプロセッサのアーキテクチャは、インテルから出ている様々な説明書や指導書によって十分に説明されている。これらのマイクロプロセッサの働きを理解するために、これらの文献を紐解くことをお勧めする。私はコードの最適化に重要な部分に特に焦点を当て、構造を簡単に記述しようと思う。
<P>命令コードはコードキャッシュから、アラインされた16バイトかたまりとして、16バイトのかたまり二つを保持できるダブルバッファに取り込まれる。命令はダブルバッファからデコーダへブロックとして移される。このブロックを ifetchブロック(instruction fetch block)と呼ぼうと思う。ifetchブロックは普通16バイト長だが、アラインされていない。ダブルバッファの用途は、16バイト境界(すなわち番地が16で割り切れる)をまたぐ命令のデコードを可能にすることである。
<P>ifetchブロックは命令長デコーダへ移され、これは各々の命令の始まりと終わりを決定する。次に命令デコーダへ移される。各々のクロックサイクル毎に三つの命令をデコードするために、三つのデコーダがある。同じクロックサイクル内でデコードされる最高三つまでの命令グループは、デコードグループと呼ばれる。
<P>デコーダは命令をマイクロオペシーション、略してμ-OPSへと翻訳する。簡単な命令は一つのμ-OPSを生成するが、その一方でもっと複雑な命令はいくつかのμ-OPSを生成するかもしれない。例えば、命令 ADD EAX,[MEM] は二つのμ-OPSを生成する。一つはソースオペランドをメモリから読み、もう一つは加算を実行する。命令をμ-OPSへと分割する目的は、システム中の後の取り扱いをより効率的にするためである。
<P>三つのデコーダはD0、D1、そしてD2と呼ばれる。D0はすべての命令を扱うことができる一方、D1とD2は一つのμ-OPSを生成する簡単な命令のみ扱える。
<P>デコーダから来たμ-OPSは、短いキューを通して、レジスタ・アロケーション・テーブル(RAT)へと移される。μ-OPSは後に常置レジスタ(EAX, EBXなど)に書かれることになるテンポラリレジスタ上で実行される。RATの目的は、μ-OPSにどのテンポラリレジスタを使うか知らせることと、レジスタ・リネーミング(後述)を可能にすることである。
<P>RATの後、μ-OPSはリオーダ・バッファ(ROB)へと送られる。ROBの用途は、アウト・オブ・オーダー実行をすることにある。μ-OPSは、必要とするオペランドが利用可能となるまでリザベーション・ステーションに止まる。もし前に生成されたμ-OPSがまだ終了してないことが原因で一つのμ-OPSが遅らされると、ROBは時間を稼ぐために今実行できる他のμ-OPSを探すかもしれない。
<P>実行の準備ができたμ-OPSは、実行ユニットへ送られ、五つのポートへ振り分けられる。ポート0と1は演算命令、ジャンプ等を扱うことができる。ポート2はメモリからの読み込みをすべて担当し、ポート3はメモリへの書き込みのための番地を生成し、ポート4はメモリへの書き込みを行う。
<P>命令の実行が終了すると、ROB内でリタイアの準備ができた印が付けられる。そしてそれはリタイアメント・ステーションへと送られる。ここでμ-OPSによって使われたテンポラリレジスタは常置レジスタに書かれる。μ-OPSはアウト・オブ・オーダー実行をしてもよいが、それらは順序よくリタイアしなければならない。
<P>次の章では、パイプラインの各々の段階でのスループットを最適化する方法の詳細について述べるつもりである。
<P>
<HR>
<A name=14>
<H2>14. 命令のデコード(PPro, PII and PIII)</H2>
私はここでは命令のデコードを、命令の取り込みよりも先に書こうと思う。というのは、命令の取り込みにおいて生ずる可能性かある遅延を理解するために、デコーダの働きを知る必要があるからである。
<P>デコーダはクロックサイクル当たり三つの命令を扱うことができるが、それは特定の条件に合致した時に限られる。デコーダD0は1クロックサイクル当たり四つまでのμ-OPSを生成する命令を扱うことができる。デコーダD1とD2は一つのμ-OPSを生成する命令しか扱うことができず、それらの命令長が8バイトを超えてはならない。
<P>同じクロックサイクルで二つあるいは三つの命令をデコードできる規則を簡単に要約してみよう。
<UL>
<LI>最初の命令(D0)が四つを超えるμ-OPSを生成しないこと。
<LI>二番目と三番目の命令は各々一つを超えるμ-OPSを生成しないこと。
<LI>二番目と三番目の命令長は各々8バイトを超えないこと。
<LI>命令が同じ16バイトのifetchブロックに収まっていなければならない(これについては次の章を参照のこと)。
</UL>
DOにおいては、命令長に制限はない(インテルの説明書は別のことを言っているが)。但し三つの命令が16バイトのifetchブロックに収まっていなければならない。
<P>生成されるμ-OPSの数が四つを超える命令のデコードには、2以上のクロックサイクルを必要とし、この間他の命令を並列にデコードすることはできない。
<P>この規則に従うと、デコードグループの最初の命令が四つのμ-OPSを生成し、次の二つが各々一つのμ-OPSを生成したとすると、デコーダは1クロックサイクル当たり最大六つのμ-OPSを生成できることになる。最小は1クロックサイクル当たり二つのμ-OPSを生成する場合で、すべての命令が二つのμ-OPSを生成する時、D1とD2は全く使われない。
<P>最大のスループットを得るために、命令を4-1-1型に従って並べることをお勧めする。二つ~四つのμ-OPSを生成する命令の間には簡単な一つのμ-OPSを生成する命令を二つ、デコード時間を増やさないという意味においてはただで挿入できる。例えば
<PRE>
MOV EBX, [MEM1] ; 1μ-OPS (D0)
INC EBX ; 1μ-OPS (D1)
ADD EAX, [MEM2] ; 2μ-OPS (D0)
ADD [MEM3], EAX ; 4μ-OPS (D0)
</PRE>
<p>
この例ではデコードに3クロックサイクル要している。命令を並べ替え、二つのデコードクループに分けることにより、1クロックサイクル稼ぐことができる。
<PRE>
ADD EAX, [MEM2] ; 2μ-OPS (D0)
MOV EBX, [MEM1] ; 1μ-OPS (D1)
INC EBX ; 1μ-OPS (D2)
ADD [MEM3], EAX ; 4μ-OPS (D0)
</PRE>
<P>
今やデコーダは2クロックサイクルで八つのμ-OPSを生成し、それは多分満足のいくものである。その後のパイプライン内のステージでは1クロックサイクル当たり三つのμ-OPSしか扱うことができないので、デコード速度がそれより速ければ、デコードがボトルネックにならないと考えられる。しかしながら、次の章で述べるように命令取り込み機構の複雑さがデコードの足かせとなることがあるので、安全のために1クロックサイクル当たりのμ-OPSの生成速度が三つを超えるように狙いを定めたいと思うだろう。
<P>各々の命令が生成するμ-OPSの数を表にしたものが<A href="#29">29章</A>にある。
<P>命令のプリフィックスもデコーダにおいてペナルティを招く可能性がある。命令は数種類のプリフィックスを持つことができる。
<UL>
<LI>オペランド・サイズ・プリフィックスは、32ビット環境から16ビットのオペランド、またはその逆を使用する際に必要になる(一種類のオペランド・サイズしか持たない命令、例えばFNSTSW AXのような命令を除く)。オペランド・サイズ・プリフィックスは、命令中に16ビットまたは32ビットの即値を持つ時、少しのクロックのペナルティが課せられる。というのは、オペランドの長さがプリフィックスによって変えられるからである。
例:
<PRE>
ADD BX, 9 ; 即値のオペランドが8ビットなのでペナルティ無し
MOV WORD PTR [MEM16], 9 ; オペランドが16ビットなのでペナルティあり
</PRE>
後の命令は次のように変えるべきである。
<PRE>
MOV EAX, 9
MOV WORD PTR [MEM16], AX ; 即値がないのでペナルティなし
</PRE>
<LI>16ビット環境で32ビット番地指定またはその逆を行うために、アドレス・サイズ・プリフィックスを用いた場合。これは滅多に必要ないし、普通回避する必要もない。アドレス・サイズ・プリフィックスは、明示的なメモリ・オペランドを使用する場合いつでもペナルティを与える(オフセット値がない場合でも)。というのは、命令コード中の r/m ビットの解釈が、プリフィックスによって変化するからである。暗黙のメモリ・オペランドのみを使用する命令、例えばストリング命令は、アドレス・サイズ・プリフィックスによってペナルティを受けない。
<LI>データの番地を指定する際、デフォルトのセグメントでない別のセグメントを用いるためにセグメント・プリフィックスが使用された場合。セグメント・プリフィックスはPPro、PIIとPIIIではペナルティはない。
<LI>リピート・プリフィックスとロック・プリフィックスはデコーダにおいてペナルティを生じない。
<LI>二つ以上のプリフィックスを用いた場合はいつもペナルティがある。普通1プリフィックス当たり1クロックのペナルティがある。
</UL>
<P>
<HR>
<A name=15>
<H2>15. 命令の取り込み (PPro, PII and PIII)</H2>
コードは、アラインされた16バイトのかたまりとしてコードキャッシュから取り込まれ、ダブル・バッファに置かれる。ダブルバッファと呼ぶのは、そのようなかたまり二つを保持できるからである。コードはダブル・バッファから取り出され、デコーダに普通16バイト長の、しかし、必ずしも16でアラインされていないブロックとして供給される。私はこれらのブロックをifetchプロック(instruction fetch blocks)と呼ぼうと思う。ifetchブロックがコードにおいて16バイト境界をまたいだ場合、ダブル・バッファの両方のブロックから取り出されなければならない。だから、ダブル・バッファの用途は、命令の取り込みが16バイト境界をまたいでもできるようにするためである。
<P>ダブル・バッファは1クロックサイクル当たり16バイトのかたまり一つを取り込むことができ、1クロックサイクル当たり一つのifetchブロックを生成できる。ifetchブロックは普通16バイト長であるが、ブロック中に予測された分岐がある場合は、短くなり得る(<A href="#22">22章</A>を参照のこと)。
<P>不幸なことに、ダブル・バッファは遅延なしにジャンプ命令の周囲のフェッチを扱うのに十分な大きさがない。ジャンプ命令を含むifetchブロックが16バイト境界をまたぐ場合、ダブル・バッファは二つのアラインされた16バイトのコードのかたまりを保持する必要がある。ジャンプ命令の後の最初の命令が16バイト境界をまたぐ場合、ダブル・バッファは有効なifetchブロックが生成できるようになるまでに二つの新しい16バイトのコードのかたまりを読まなければならない。これは、最悪の場合、ジャンプ命令の後の最初の命令が2クロックサイクル遅らされることを意味する。つまりジャンプ命令を含むifetchブロック中の16バイト境界のために1クロックサイクル、そしてジャンプ命令の後の最初の命令の16バイト境界のために1クロックサイクルのペナルティが課せられる。ジャンプ命令を含むifetchブロック中に二つ以上のデコードグループがある場合、賞与を得ることができる。というのは、ジャンプ命令の後で命令に先立ち一つまたは二つの16バイト単位のコードを取り込む余計な時間があるからである。この賞与は、後述する表に従ってペナルティを償えることがある。ジャンプ命令の後でダブル・バッファが16バイトのコードのかたまりを一つだけ取り込んだとすると、ジャンプ命令の後の最初のifetchブロックはこのかたまりと同一、つまり16バイト境界にアラインされる。言い換えれば、ジャンプ命令の後の最初のifetchブロックは最初の命令で始まるわけではなく、16で割り切れる直前の番地から始まる。ダブル・バッファが16バイトのコードのかたまりを二つ読み込める時間があれば、新しいifetchブロックは16バイト単位をまたぐことができ、ジャンプ命令の後の最初の命令から始まる。これらの規則を次の<A name="ifetchtable">表</A>に要約した。
<PRE>
ジャンプ命令 ifetchブロッ ジャンプ命令 ジャンプ命令
を含むifetch クに含まれる の後の最初の デコーダの のあとの最初
ブロックの数 16バイト境界 命令中の16バ 遅延 のifetchのア
の数 イト境界の数 ラインメント
------------------------------------------------------------------
1 0 0 0 16
1 0 1 1 命令
1 1 0 1 16
1 1 1 2 命令
2 0 0 0 命令
2 0 1 0 命令
2 1 0 0 16
2 1 1 1 命令
3以上 0 0 0 命令
3以上 0 1 0 命令
3以上 1 0 0 命令
3以上 1 1 0 命令
</PRE>
<P>ジャンプ命令は命令取り込みを遅らせるので、ループは常に、その中に含まれる16バイト境界の数より少なくとも2クロックサイクル余計にかかる。
<P>命令取り込み機構に伴う更なる問題は、新しいifetchブロックは、先のブロックが使い尽くされるまで生成されないことである。各々のifetchブロックはいくつかのデコードグループを含むことができる。16バイト長のifetchブロックが未完結の命令で終わっていると、次のifetchブロックはその命令で始まる。最初の命令は常にデコーダDOへ行き、次の二つの命令はもし可能であればD1とD2に行く。この結果、D1とD2が使われるのは最適より少なくなる。コードの構造が推薦できる4-1-1型をしていて、D1とD2に行くように意図されている命令がたまたまifetchブロックの最初の命令になったとすると、その命令はD0に行かなければならず、1クロックサイクルが無駄になる。これは多分ハードウェアの設計ミスである。少なくともこれは次善のデザインである。この問題の結果として、コード片のデコードにかかる時間は、最初のifetchブロックが始まる位置にかなり依存して変化する。
<P>デコード速度が重要で、これらの問題を避けたいと思うならば、各々のifetchブロックがどこから始まるか知る必要がある。これは全く退屈な仕事である。最初にコードセグメントがパラグラフ(訳注: 16バイト)でアラインされるようにする必要がある。これは16バイト境界がどこにあるか知るために必要である。次にアセンブラからの出力リストを見て、命令長を知らなければならない(命令のコードのされ方を知り、それで命令長を予測する勉強をお勧めする)。一つのifetchブロックの始まる位置がわかれば、次のifetchブロックを次のように見つけることができる。ブロックを16バイト長にする。それが命令の境界で終われば、次のブロックはそこから始まる。命令で終わっていなければ、次のブロックはこの命令から始まる(ここでは命令長だけを数えればよい。いくつのμ-OPSが生成され、それらがどう働くかについては気にしなくてよい)。このようにしてコードのすべてを調べ、各々のifetchブロックの始まりに印をつけることができる。唯一の問題は、どこから始めるかを知ることである。一つのifetchブロックが始まる位置がわかれば、それに続くすべてのブロックがわかる。しかし最初のブロックがどこから始まるか知らなければならない。これからいくつかの指針を示す:
<UL>
<LI>ジャンプ、コール、またはリターンの後の最初のifetchブロックは、最初の命令または16バイト境界に最も近い前の、つまり上の表に従った位置にある。最初の命令を16バイト境界で始まるようにアラインすれば、最初のifetchブロックはそこから始まると見て間違いない。重要なサブルーチンとループの入り口をこの目的のために16バイト境界で始まるようにしたくなるかもしれない。
<LI>二つの連続した命令を足した長さが16バイトを超えるならば、二つ目の命令は一つ目と同じifetchブロックには収まらないと確信できる。結果としてifetchブロックは常に二つ目の命令から始まるはずである。これを、引き続くifetchブロックの始まりを見つけるために、スタート地点として使える。
<LI>分岐予測ミスの後の最初のifetchブロックは16バイト境界から始まる。<A href="#22_2">22章2節</A>に説明してあるように、5回を超えて繰り返すループは、抜けるときにいつも分岐予測ミスを起こす。それ故このようなループの後の最初のifetchブロックは直前の16バイト境界にある。
<LI>他の次のようなイベントも16バイト境界のifetchブロックの原因となる。それは、割り込み、例外、自己改変コード、それからCPUID命令、IN命令、OUT命令である。
</UL>
<P>
例が見たいと思っているに違いない:
<PRE>
番地 命令 命令長 μ-OPS 予期されるデコーダ
----------------------------------------------------------------------
1000h MOV ECX, 1000 5 1 D0
1005h LL: MOV [ESI], EAX 2 2 D0
1007h MOV [MEM], 0 10 2 D0
1011h LEA EBX, [EAX+200] 6 1 D1
1017h MOV BYTE PTR [ESI], 0 3 2 D0
101Ah BSR EDX, EAX 3 2 D0
101Dh MOV BYTE PTR [ESI+1],0 4 2 D0
1021h DEC ECX 1 1 D1
1022h JNZ LL 2 1 D2
</PRE>
<P>
最初のifetchブロックが1000H番地で始まり、1010H番地で終わると仮定しよう。これは MOV [MEM],0 の命令終わりの前なので、次のifetchブロックは1007H番地で始まり、1017H番地で終わるはずである。これは命令の境界なので、三番目のifetchブロックは1017Hで始まり、ループの残りをまかなうはずである。これをデコードするのにかかるクロックサイクル数はD0で解釈される命令数であり、LLのループでは繰り返し当たり5クロックサイクルである。最後の五つの命令をまかなうのは三つのデコード・ブロックを含むifetchブロックであり、16バイト境界上(1020H)に乗る。前記した表より、ジャンプ命令の後の最初のifetchブロックはジャンプ命令の後の最初の命令で始まり、それは1005H番地のラベルLLであり、1015H番地で終わる。これはLEA命令の前で終わるので、次のifetchブロックは1011H番地から1021H番地になり、残りのブロックで1021Hまでをまかなう。今LEA命令とDEC命令の両方がifetchブロックの始まりに降りてきて、D0に行かされる。D0でデコードされる命令は七つになったので、次の繰り返し時には7クロックサイクルかかる。最後のifetchブロックは一つのデコードグループ(DEC ECX / JNZ LL)だけを含み、これは16バイト境界にない。表に従えば、ジャンプ命令の後の次のifetchブロックは16バイト境界で始まり、これは1000H番地である。これは最初の繰り返しの状況と同じであり、つまりこのループのデコードには5クロックサイクルと7クロックサイクル交互にかかる。他のボトルネック原因がないので、完全なループ実行には1000回の繰り返しで6000クロックかかるはずである。開始番地が違えば、最初と最後の命令に16バイト境界が来て、その結果8000クロックかかるはずである。D1とD2の命令がifetchブロックの最初には一つも来ないように命令を並べ替えれば、5000クロックしかかからないようにできる。
<P>上の例は慎重に計画すればコードの取り込みとデコードだけがボトルネックである。この問題を最も簡単に避ける方法は、クロックサイクル当たり四つ以上のμ-OPSを生成するように構成し、ここに記述したペナルティがボトルネックにならないようにすることである。小さなループではこれが不可能なことがあり、命令の取り込みとデコードを最適化するための方法を見つけ出さなくてはならない。
<P>可能な一つの方法は、望ましくない16バイト境界を避けるため、手続きの開始番地を変えることである。境界が分かるように、コードセグメントをパラグラフでアラインしたことを思い出して欲しい。
<P>ALIGN 16ディレクティブをループの入り口の前に挿入することにより、アセンブラはNOP命令と他の詰め物としての命令を入れ、最も近い16バイト境界まで埋めてくれる。XCHG EBX,EBXは2バイトの詰め物(いわゆる2バイトのNOP)である。誰が考えたのか知らないが、ほとんどのプロセッサでこれは二つのNOPよりも余計な時間がかかる! ループが長く実行されれば、ループの外側にあるいかなるものも速度にとって重要ではなく、準最適な詰め物命令を気にすることはない。しかし詰め物によってかかる時間が重要ならば、詰め物の命令を手で選んでもよい。その上、詰め物は何か役に立つ使い方をしてもよい。例えばレジスタ・リード・ストール(<A href="#16_2">16章2節</A>を参照のこと)を避けるためにレジスタをリフレッシュするなどがある。例として、EBPレジスタを番地指定に使うが滅多に更新しないならば、レジスタ・リード・ストールの可能性を減らすためにMOV EBP,EBPやADD EBP,0を詰め物として使用してよい。何も役に立たなくてもよいなら、FXCH ST(0)はよい詰め物である。というのはこの命令はどの実行ポートにも読み込みをしないからである。但し、ST(0)に有効な浮動小数点数が入っていなければならない。
<P>他の治療法として、命令を、結果が影響を受けないように並べ替える方法がある。これは全く難しいパズルになりがちで、いつも満足のいく答えが見つかるとは限らない。
<P>さらに他の可能性として、命令長を操作する方法がある。一つの命令を長さの異なる他の命令に置き換えられることがある。多くの命令が異なった長さの異なったバージョンでコードされる。アセンブラは可能な限りいつも最も短いバージョンの命令を選択する。しかししばしば、長いバージョンをハードコードできる。例えば、DEC ECXは1バイト長で、SUB ECX,1は3バイト長である。また、次のトリックを使って長い即値のバージョンで書くことができる。
<PRE>
SUB ECX, 9999
ORG $-4
DD 1
</PRE>
<P>メモリ・オペランドを伴った命令はSIBバイトで1バイト長くできる。しかし1バイト長い命令を作る最も簡単な方法はDS:セグメント・プリフィックス(DB 3EH)を付け加えることである。マイクロプロセッサは命令長が15バイトを超えない限り、冗長で無意味なプリフィックス(ロック・プリフィックスを除く)を受け入れる。メモリ・オペランドを含まない命令でさえ、セグメント・プリフィックスを持つことができる。だから、DEC ECXを2バイト長で書くことができる。
<PRE>
DB 3Eh
DEC ECX
</PRE>
<P>命令が二つ以上のプリフィックスを持つと、デコーダでペナルティが課せられることを忘れてはならない。無意味なプリフィックスを持つ命令、特にリピート・プリフィックスとロック・プリフィックスは、命令コードにこれ以上空きがなくなったら、将来のプロセッサで使われる可能性がある。しかし、セグメント・プリフィックスはどの命令に使っても安全だと思う。
<P>これらの方法により普通は、ifetch境界を望む所に置くことができる。それが退屈なパズルになりがちだとしても。
<P>
<HR>
<A name=16>
<H2>16. レジスタ・リネーミング (PPro, PII and PIII)</H2>
<A name=16_1>
<H3>16.1 依存関係の解消</H3>
レジスタ・リネーミングはこれらのマイクロプロセッサによって使われる進歩した技術で、異なるコード部分の間の依存関係を取り除くために使われる。例:
<PRE>
MOV EAX, [MEM1]
IMUL EAX, 6
MOV [MEM2], EAX
MOV EAX, [MEM3]
INC EAX
MOV [MEM4], EAX
</PRE>
<P>ここで、最初の三つの命令からはどんな結果も必要としないという意味において、最後の三つの命令は最初の三つの命令と独立しているので。初期のプロセッサで最適化するためには、最後の三つの命令にEAXと異なるレジスタを使用し、命令を並べ替えて、最後の三つの命令が最初の三つの命令と並列に実行されるようにしなければならなかっただろう。PPro、PIIとPIIIプロセッサではこれを自動的に行ってくれる。EAXに書き込む度に、新しいテンポラリレジスタが割り当てられる。この結果、MOV EAX,[MEM3]命令は先の命令に依存しなくなる。アウト・オブ・オーダー実行により、[MEM]への移動は、遅いIMUL命令より先に終わるだろう。
<P>レジスタ・リネーミングは完全に自動的に行われる。命令がレジスタに書き込む度に、常置レジスタの代わりに新しいテンポラリレジスタが割り当てられる。レジスタからの読み出しと書き込みの両方を行う命令もリネーミングの原因となる。例えば、上のINC EAX命令は、一つのテンポラリレジスタを入力用に、もう一つのテンポラリレジスタを出力用に割り当てる。これはもちろん独立性を損なうことはない。しかし、後に説明するように、連続したレジスタの読み出しには、いくつかの重要な点がある。
<P>すべての一般用レジスタ、即ちスタックポインタ、フラグ、浮動小数点レジスタ、MMXレジスタ、XMMレジスタとセグメントレジスタはリネームできる。コントロールレジスタと浮動小数点ステータスレジスタはリネームできない。これが、これらのレジスタの使用が遅い原因である。40個の万能テンポラリレジスタがあるので、すべてのテンポラリレジスタを使い切ってしまうことはありそうにない。
<P>レジスタを0にする一般的な方法は、XOR EAX,EAXまたはSUB EAX,EAXである。これらの命令は先に入っていた値と独立とは見なされない。先にある遅い命令への依存を取り除くには、MOV EAX,0命令を使えばよい。
<P>レジスタ・リネーミングはレジスタ・エイリアス・テーブル(RAT)とリオーダ・バッファ(ROB)によって制御される。デコーダからのμ-OPSはキューを通ってRATへ送られる。そしてROBとリザベーション・ステーションへと送られる。RATは1クロックサイクル当たり三つのμ-OPSを扱うことができる。これは、マイクロプロセッサのスループットは、平均して1クロックサイクル当たり三つのμ-OPSを超えられないということである。
<P>リネーミングの数の実際的な制限はない。RATは1クロックサイクル当たり三つのレジスタをリネームでき、同じレジスタであっても1クロックサイクルに三回リネームできる。
<P>
<H3><A name=16_2>16.2</A> レジスタ・リード・ストール</H3>
しかし、全く深刻な、別の制限がある。それは、1クロックサイクル当たり二つの常置レジスタからしか読み出すことができないということである。この制限は命令によって使われる全てのレジスタに適用される。但し、レジスタに書き込むのみの命令を除く。例:
<PRE>
MOV [EDI + ESI], EAX
MOV EBX, [ESP + EBP]
</PRE>
<P>最初の命令は二つのμ-OPSを生成する。一つはEAXから読み出し、もう一つはEDXとESIから読み出す。次の命令は一つのμ-OPSを生成する。それはESPとEBPからの読み出す。EBXは命令が書き込むだけなので、読み出しには数えない。この三つのμ-OPSが一緒にRATに行ったと仮定しよう。私はRATに一緒に行く連続した三つのμ-OPSのグループに三つ組という言葉を使おうと思う。ROBが1クロックサイクル当たりに読み出せる常置レジスタは二つだけで、我々の三つ組は五つのレジスタ読み出しが必要なので、この三つ組がリザベーション・ステーションに来るまでに余計な2クロックサイクルぶん遅れるだろう。三つ組中で三つまたは四つのレジスタを読み出すならば、1クロックサイクル遅れるだろう。
<P>同じ三つ組中で同じレジスタを、回数に数えられずに2回以上読み出すことができる。上の命令を次のように変えると、
<PRE>
MOV [EDI + ESI], EDI
MOV EBX, [EDI + EDI]
</PRE>
二つのレジスタを読み出すだけで済み(EDIとESI)、三つ組は遅延を受けない。
<P>実行中断中のμ-OPSが書き込む予定のレジスタはROBに保存されているので、それが書き戻されるまで自由に読み出すことができる。書き戻しには少なくとも3クロックサイクル、普通はもっとかかる。書き戻しは最後の実行ステージで行われ、ここで値が利用可能となる。他の言い方をすれば、実行ユニットからその値がまだ出力できていないなら、RAT中ののレジスタはストールせずにいくつでも読み出せるということである。この理由は、値が利用可能になると直ちに、それを必要とするすべての後続のROBエントリに直接書き込まれるからである。しかし値が一旦テンポラリレジスタまたは常置レジスタに書き戻されると、その値を必要とする後続のμ-OPSがRATに行ったとき、値をレジスタ・ファイルから読み出さなければならない。そしてそれは二つの読み出しポートしか持っていない。RATから実行ユニットまでには三つのパイプライン・ステージがあり、一つの三つ組μ-OPS中で書かれるレジスタは、少なくとも次の3個の三つ組では自由に読み出すことができると確信してよい。書き戻しが、並べ替え、遅い命令、依存の連続、キャッシュミス、その他任意の種類のストールによって遅れる場合、レジスタから自由に読み出せる期間は命令ストリームの下流に延びる。
<P>例:
<PRE>
MOV EAX, EBX
SUB ECX, EAX
INC EBX
MOV EDX, [EAX]
ADD ESI, EBX
ADD EDI, ECX
</PRE>
<P>これら六つの命令は各々一つのμ-OPSを生成する。最初の三つのμ-OPSが一緒にRATに行ったとしよう。これらの三つのμ-OPSはレジスタEBX, EBX, EAXを読み出す。しかしEAXは読み出す前に書き込もうとするため、読み出しは自由でストールはない。次の三つのμ-OPSはEAX, ESI, EBX, EDI, ECXから読み出す。EAX, EBXとECXは先の三つ組によって変更されており、まだ書き戻してないため、これらは自由に読み出せる。それでESIとEDIだけが読み出しの対象となり、次の三つ組でもストールを受けない。最初の三つ組のSUB ECX,EAXをCMP ECX, EAXと置き換えると、ECXは書かれないので次の三つ組ESI, EDI, ECXの読み出し時にストールを受ける。同様に、INC EBXをNOPか何かで置き換えると、次の三つ組でESI, EBX, EDXの読み出し時にストールを受ける。
<P>どのμ-OPSも、三つ以上のレジスタから読み出すことはできない。それゆえ、三つ以上のレジスタを読み出す全ての命令は二つ以上のμ-OPSに分解される。
<P>レジスタの読み出し数を数えるには、命令によって読み出されるレジスタの数を含めなければならない。これは整数レジスタ、フラグレジスタ、スタックポインタ、浮動小数点レジスタ、そしてMMXレジスタである。XMMレジスタは二つのレジスタと数える。但し、一部分だけを使う場合を除く。例えば、ADDSSとMOVHLPS命令である。セグメントレジスタと命令ポインタは数えない。例えば、SETZ ALにおいて、フラグレジスタは数えるが、ALは数えない。ADD EBX, ECXでは、EBXとECXの両方を数える。しかしフラグレジスタは数えない。なぜならフラグレジスタは書き込まれるだけだからである。PUSH EAXはEAXとスタックポインタを読み込み、スタックポインタに書き込む。
<P>FXCH命令は特別な場合である。これはリネーミングによって動くが、どんな値も読み出さない。それでレジスタ・リード・ストールの規則に数えない。FXCH命令はレジスタの読み書きを一切しないので、レジスタ・リード・ストールの規則に関しては一つのμ-OPSのように振る舞う。
<P>μ-OPS三つ組と、デコードグループを混同しないでいただきたい。デコードグループは一つから六つまでのμ-OPSを生成する。デコードグループが三つの命令を持ち、三つのμ-OPSを生成する時でさえ、三つのμ-OPSが一緒にRATに行くという保証はない。
<P>デコーダとRAT間のキューは大変短い(十個のμ-OPS)ので、レジスタ・リード・ストールがデコーダをストールさせたり、デコーダのスループットの変動がRATをストールさせたりしないとは見なせない。
<P>キューが空でない限り、どのμ-OPSが一緒にRATに行くかを予測することは大変難しい。最適化されたコードでは分岐予測ミスが起きた直後でだけキューが空になっているべきである。同じ命令によって生成されるいくつかのμ-OPSは必ずしも一緒にRATに行くわけではない。というのは、μ-OPSは単にキューから順番に、一度に三つずつ取り上げられるからである。この連続は予測された分岐によって壊されることはない。ジャンプの前後のμ-OPSも一緒にRATに行くことができる。予測ミスした分岐でだけは、キューの内容が捨られて最初からやり直しになるので、次の三つのμ-OPSは確実に一緒にRATに行く。
<P>三つの連続するμ-OPSが三つ以上の異なるレジスタから読み出す時、当然それらは一緒にRATを通らないほうがよいと思うだろう。一緒に通る確率は1/3である。三つ組のμ-OPS中で、書き戻されたレジスタから三つまたは四つ読み出すことによるペナルティは、1クロックサイクルである。1クロックの遅れは、RATを通してあと三つのμ-OPSを読み出すのと等価と思ってよい。三つのμ-OPSが一緒にRATに行く確率が1/3であるため、ペナルティの平均は3/3=1μ-OPSと等価になるはずである。コード片がRATを通るのにかかる平均時間を計算するためには、レジスタ・リード・ストールを起こす可能性のある数をμ-OPSに加算し、3で割ればよい。どのμ-OPSが一緒にRATに行くかが確実にわかっているか、または余計な一つの命令によって二つ以上のレジスタ・リード・ストールの可能性を防ぐのでなければ、余計な命令を一つ挿入することによってストールを取り除くことは引き合わないと分かるであろう。
<P>1クロック当たり三つのμ-OPSのスループットを狙う状況では、1クロックサイクル当たり二つしか常置レジスタから読み出せない制限は、扱うのに問題となるボトルネックであろう。レジスタ・リード・ストールを取り除く可能性のある方法は、
<UL>
<LI>同一のレジスタを読み出すμ-OPSは、同じ三つ組に入りやすいように互いに近くに置く。
<LI>異なったレジスタから読み出すμ-OPSは、同じ三つ組みに入れないように距離を取る。
<LI>レジスタから読み出すμ-OPSは、レジスタに書き込むか変更する命令のせいぜい3~4個の三つ組以内の範囲に置くようにする。これは、レジスタに書き戻される前に値を読むことを確実にするためである(この間にジャンプ命令があっても、それが予測されたものならば気にしなくてよい)。レジスタへの書き込みを遅らせる何らかの理由があるならば、レジスタの読み出しは命令ストリームの幾分先にあっても安全であろう。
<LI>ポインタの代わりに絶対番地を使うようにする。これはレジスタの読み出し回数を減らすためである。
<LI>一つまたはそれ以上後の三つ組の中でリード・ストールの原因とならないようにするため、ストールを起こさない三つ組の中でレジスタをリネーム(訳注: 書き込んでリネームが起きるように)してもよい。例:
<PRE>
MOV ESP,ESP / ... / MOV EAX,[ESP+8]
</PRE>
この方法は余分なμ-OPSが必要であり、予測されるリード・ストールの数の平均が1/3を超えないと引き合わない。
</UL>
<P>二つ以上のμ-OPSを生成する命令において、レジスタ・リード・ストールの可能性の正確な分析をするため、命令から生成されるμ-OPSの順序が知りたいかもしれない。それで、最も普通の例を下に並べてみた。
<P>
<U>メモリへの書き込み</U><BR>
メモリへの書き込みは二つのμ-OPSを生成する。一つ目(ポート4へ)はストア動作で、ストアするレジスタを読み出す。二つ目(ポート3)はポインタレジスタを読み出してメモリの番地を計算する。例:<BR>
<PRE>
MOV [EDI], EAX
</PRE>
最初のμ-OPSはEAXを読み出し、二番目のμ-OPSはEDIを読み出す。
<PRE>
FSTP QWORD PTR [EBX+8*ECX]
</PRE>
最初のμ-OPSはST(0)を読み出し、二番目のμ-OPSはEBXとECXを読み出す。
<P>
<U>読み出しと変更</U><BR>
メモリ・オペランドを読み出して、何らかの演算または論理操作によりレジスタを変更する命令は、二つのμ-OPSを生成する。最初のμ-OPS(ポート2)はロード命令で、ポインタレジスタを読み出す。二番目のμ-OPSは演算命令(ポート0または1)で、被演算レジスタの読み出しと書き込みを行い、フラグへの書き込みの可能性もある。例:
<PRE>
ADD EAX, [ESI+20]
</PRE>
最初のμ-OPSはESIを読み出し、二番目のμ-OPSはEAXを読み出して、EAXとフラグに書き込む。
<P>
<U>読み出し/変更/書き込み</U><BR>
読み出し/変更/書き込み命令は四つのμ-OPSを生成する。最初のμ-OPS(ポート2)は何らかのポインタレジスタを読み出し、二番目のμ-OPS(ポート0または1)はソースレジスタから読み出しと書き込み(訳注: 一時結果の書き込みか)を行い、フラグへの書き込みの可能性がある。三番目のμ-OPS(ポート4)は一時的な結果だけ(ここでは数に入れない)を読み出し、四番目のμ-OPS(ポート3)は再度何らかのポインタレジスタを読み出す。最初のμ-OPSと四番目のμ-OPSは一緒にRATに入れないため、同一のポインタレジスタを用いることによる有利性がない。例:<BR>
<PRE>
OR [ESI+EDI], EAX
</PRE>
最初のμ-OPSはESIとEDIを読み出す。二番目のμ-OPSはEAXを読み出しEAXとフラグに書き込む。三番目のμ-OPSは一時的な結果だけを読み出す。四番目のμ-OPSはESIとEDIを再度読み出す。これらのμ-OPSがたとえどのようにRATに入ろうと、EAXを読み出すμ-OPSは、ESIとEDIを読み出すμ-OPSのいずれか一方と一緒に行くと確信してよい。それゆえレジスタ・リード・ストールはこの命令においては、これらのレジスタのどちらかが最近変更されていなければ避けることができない。
<P>
<U>レジスタのプッシュ</U><BR>
レジスタのプッシュ命令は三つのμ-OPSを生成する。最初のμ-OPS(ポート4)は、レジスタを読み出すストア命令である。二番目のμ-OPS(ポート3)はスタックポインタを読み出し、番地を生成する。三番目のμ-OPS(ポート0または1)はスタックポインタを読み出して変更し、ワードの大きさをスタックポインタから引く。
<P>
<U>レジスタのポップ</U><BR>
レジスタのポップ命令は二つのμ-OPSを生成する。最初のμ-OPS(ポート2)は値のロードで、スタックポインタを読み出してレジスタに書き込む。二番目のμ-OPS(ポート0または1)はスタックポインタを読み出して変更し、スタックポインタを調整する。
<P>
<U>呼び出し(コール)</U><BR>
nearコールは四つのμ-OPSを生成する(ポート1, 4, 3, 01)。最初の二つのμ-OPSはIPレジスタから読むだけ(リネームできないので数に入らない)である。三番目のμ-OPSはスタックポインタを読み出す。最後のμ-OPSはスタックポインタを読み出しで変更する。
<P>
<U>復帰(リターン)</U><BR>
nearリターンは四つのμ-OPSを生成する(ポート2, 01, 01, 1)。最初のμ-OPSはスタックポインタを読み出す。三番目のμ-OPSはスタックポインタを読み出しで変更する。
<P>
レジスタ・リード・ストールを避ける方法は例2.6に書いてある。
<P>
<HR>
<A name=17>
<H2>17. アウト・オブ・オーダー実行 (PPro, PII and PIII)</H2>
リオーダ・バッファ(ROB)は40個のμ-OPSを保持できる。各々のμ-OPSはすべてのオペランドの準備ができ、実行ユニットに空きができるまでROBで待機する。これはアウト・オブ・オーダー実行を可能にする。キャッシュ・ミスによる遅延がコード部分に生じた時も、それより後のコード部分が遅らされた操作と独立であれば、それらが遅延を受けることはない。
<P>メモリへの書き込みは、他の書き込みと比べてアウト・オブ・オーダー実行することはできない。四つの書き込みバッファがあるので、多くのキャッシュ・ミスやキャッシュされていないメモリに書き込んでいることが予期されるならば、四つの書き込みを同時に行い、次の四つの書き込みの前にプロセッサに何か他の仕事を確実にさせるようにスケジュールすることを勧める。メモリの読み出しと他の命令は、IN命令、OUT命令、その他のシリアル化命令を除いてアウト・オブ・オーダー実行できる。
<P>コードがメモリへの書き込みをし、そのすぐ後に同じ番地から読み出しをする場合、読み出しは誤って書き込みよりも先に実行されることがある。というのは、ROBは並べ替えをしている時はメモリの番地がわからないからである。このエラーは書き込み番地が計算される時に検出され、読み出し動作(これは投機実行であった)は再実行されなければならない。このペナルティはおおよそ3クロックである。このペナルティを避ける唯一の方法は、同じ番地のメモリへの書き込みと引き続く読み出しの間に実行ユニットに他の仕事を確実にさせることである。
<P>五つのポートの周りにはいくつかの実行ユニットが配置されている。ポート0と1は演算操作などを行う。単純移動、演算と論理操作はポート0と1の両方ででき、どちらか空いた方で先に実行される。ポート0は乗算、除算、整数シフトと回転、そして浮動小数点操作も扱える。ポート1はジャンプといくつかのMMX, XMM操作を行うことができる。ポート2はメモリから読み出しのすべてと少しのストリング命令、XMM命令を扱うことができる。ポート3はメモリ書き込みの番地の演算を行う。ポート4はすべてのメモリ書き込みの操作を行う。<A href="#29">29章</A>に命令によって生成されるμ-OPSと、それらが行くであろうポートの完全な表がある。すべてのメモリ書き込み命令は二つのμ-OPS、一つはポート3、もう一つはポート4、を必要とすることに注意して欲しい。それに比べメモリ読み出し命令は一つのμ-OPSだけ(ポート2)を使用する。
<P>ほとんどの場合、各々のポートは1クロックサイクル当たり一つの新しいμ-OPSを受け取ることができる。これは、五つのμ-OPSが別々のポートに行けば、同じクロックサイクル内に五つのμ-OPSまで実行できることを意味している。しかしパイプラインの最初のほうで1クロック当たり最大三つまでのμ-OPSまでという制限があるため、平均して1クロック当たり三つのμ-OPSを超えて実行することはできない。
<P>1クロック当たり三つのμ-OPSのスループットを維持したいならば、どの実行ポートもμ-OPSの1/3を超えて受け取ることはないということを確かめる必要がある。<A href="#29">29章</A>のμ-OPSの表を用い、各々のポートに行くμ-OPSの数を数えよ。ポート0と1が満たされており、ポート2が空いていれば、ポート0と1からのロードのどちらかをポート2からのロードにするため、MOV レジスタ, レジスタまたはMOV レジスタ, 即値の命令のどちらかをMOV レジスタ, メモリ命令に置き換えることにより、コードを改善することができる。
<P>大部分のμ-OPSは実行に1クロックサイクルしかかからないが、乗算、除算、そして多くの浮動小数点命令はもっとかかる。
<P>浮動小数点の加算と減算には3クロックサイクルかかるが、先の命令が終了する前に新しいFADDやFSUB命令を受け取るように実行ユニットは完全にパイプライン化されている(もちろん、それらが独立である時だが)。
<P>整数の乗算には4クロックかかり、浮動小数点の乗算は5クロック、MMX操作は3クロックかかる。整数とMMXの操作は、クロックサイクル毎に新しい命令を受け取ることができるようにパイプライン化されている。浮動小数点の乗算は部分的にパイプライン化されている。実行ユニットは新しいFMUL命令を先の命令の2クロック後に受け取ることができる。それで最大のスループットは2クロックサイクル当たり一つのFMULである。FMUL同士の間を整数の乗算で埋めることはできない。というのは、それらは同じ回路を使うからである。XMMの加算と乗算はそれぞれ3クロックと4クロックで、完全にパイプライン化されている。しかし各々の論理XMMレジスタは二つの64ビット物理レジスタとして実装されているため、パック化されたXMM操作に二つのμ-OPSを必要とし、スループットは2クロックサイクル当たり一つのXMM演算命令である。XMMの加算と乗算命令は並列に実行できる。というのは、それらは同じ実行ポートを使わないからである。
<P>整数と浮動小数点の除算は最大39クロックかかり、パイプライン化されていない。これは先の除算が終わらないと実行ユニットは新しい除算を始めることができないということを意味している。同じ事が平方根と超越数の関数に適用される。
<P>ジャンプ命令、呼び出し(コール)命令、復帰(リターン)命令も同様に完全にパイプライン化されていない。先のジャンプ命令の1クロックサイクル後に新しいジャンプを始めることはできない。それでジャンプ命令、呼び出し命令、復帰命令の最大スループットは2クロックサイクル当たり1命令となる。
<P>もちろん、多くのμ-OPSを生成する命令は避けなければならない。LOOP XX命令は、例えば、DEC ECX / JNZ XXで置き換えるべきである。
<P>連続したPOP命令は、μ-OPSの数を減らすために分解してもよい。例えば
<PRE>
POP ECX / POP EBX / POP EAX ; これは次のように替えられる
MOV ECX,[ESP] / MOV EBX,[ESP+4] / MOV EAX,[ESP] / ADD ESP,12 ; 訳注: MOV EAX,[ESP+8]の間違いであると思われる
</PRE>
先のコードは六つのμ-OPSを生成するが、後の命令はわずか四つのμ-OPSを生成し、デコードが速い。PUSH命令を同じように分解することは有利でない。というのは分解されたコードは、それらの間に別の命令を挿入するか、レジスタが最近リネームされていないと、レジスタ・リード・ストールを起こしがちだからである。呼び出し(コール)命令と復帰(リターン)命令を同様に分解すると、リターン・スタック・バッファの予測の妨げになる。ADD ESP命令は初期のプロセッサではAGIの原因となることにも注意せよ。
<P>
<HR>
<A name=18>
<H2>18. リタイアメント (PPro, PII and PIII)</H2>
リタイアメントは、μ-OPSによって使われたテンポラリレジスタを常置レジスタ(EAX, EBXなど)に移す処理である。μ-OPSが実行されてしまうと、それはROB内でリタイアの準備ができた印が付けられる。
<P>リタイアメント・ステーションは1クロックサイクル当たり三つのμ-OPSを扱うことができる。RAT内でスループットは既に1クロック当たり三つのμ-OPSに制限されているので、これは問題に見えないかもしれない。しかし、リタイアメントは二つの理由のためにまだなおボトルネックになりうる。まず、命令は順序正しくリタイアしなければならない。μ-OPSをアウト・オブ・オーダー実行すると、順序に従ったすべての先行するμ-OPSがリタイアしないと、リタイアできない。二番目の制限は、ジャンプ命令はリタイアメント・ステーションの三つのスロットの内最も最初にリタイアしなければならないことである。丁度次の命令がD0だけに収まる場合D1とD2デコーダが空になるように、次のリタイアするμ-OPSがジャンプ命令で取られるとリタイアメント・ステーション内の後の二つのスロットが空になる。これは、μ-OPSの数が3で割り切れない命令を持つ小さなループの場合重大である。
<P>リオーダ・バッファ(ROB)内の全てのμ-OPSはリタイアするまでとどまる。ROBは40個のμ-OPSを保持できる。これは長い遅延を伴う命令または遅い命令の間に実行できる命令の数を制限する。除算が終了する前にROBはリタイアを待つ実行されたμ-OPSで満たされる。除算が終了、リタイアした時にのみ、連続するμ-OPSはリタイアを開始できる。というのはリタイアは順序に従って実行されるからである。
<P>予測分岐の投機実行の場合(<A href="#22">22章</A>を見よ)、投機実行されたμ-OPSは予測が正しいことが確実になるまでリタイアすることができない。予測が外れた時は投機実行されたμ-OPSはリタイアすることなく捨てられる。
<P>次の命令は投機実行できない。メモリへの書き込み、IN, OUT命令、シリアル化命令がそうである。
<P>
<HR>
<A name=19>
<H2>19. パーシャル・ストール (PPro, PII and PIII)</H2>
<A name=19_1>
<H3>19.1 パーシャル・レジスタ・ストール</H3>
パーシャル・レジスタ・ストールは、32ビットレジスタの一部分に書いた後、レジスタ全体または書いたサイズより大きいサイズで読み出そうとした時に起きる問題である。例えば<PRE>
MOV AL, BYTE PTR [M8]
MOV EBX, EAX ; パーシャル・レジスタ・ストール
</PRE>
<P>
これは5~6クロックの遅延を伴う。この理由は、ALにテンポラリレジスタが割り当てられるからである(AHとは独立するように)。実行ユニットはALとEAXの残りの値とを結合できるようになる前に、ALへの書き込みがリタイアするまで待たなければならない。ストールはコードを次のように替えることによって避けられる。
<PRE>
MOVZX EBX, BYTE PTR [MEM8]
AND EAX, 0FFFFFF00h
OR EBX, EAX
</PRE>
<P>もちろんパーシャル・ストールは、パーシャル・レジスタに書いた後に別の命令を挿入することによって避けることもできる。これはフルサイズのレジスタを読む前に、リタイアまでの時間を稼ぐためである。
<P>違うサイズのデータ(8, 16, そして32ビット)を混ぜる時はいつも、パーシャル・ストールが起きることに気を付けておくべきである。
<PRE>
MOV BH, 0
ADD BX, AX ; ストールあり
INC EBX ; ストールあり
</PRE>
<P>
フルサイズのレジスタ、またはより大きなサイズでレジスタに書き込んだ後でパーシャル・レジスタを読み出しても、ストールは生じない。
<PRE>
MOV EAX, [MEM32]
ADD BL, AL ; ストールなし
ADD BH, AH ; ストールなし
MOV CX, AX ; ストールなし
MOV DX, BX ; ストールあり
</PRE>
<P>パーシャル・レジスタ・ストールを避ける最も簡単な方法は、いつもフルサイズのレジスタを用い、より小さなメモリ・オペランドより読み出す時にMOVZXまたはMOVSXを用いることである。これらの命令はPPro, PIIとPIIIでは速いが、初期のプロセッサでは遅い。それゆえ、すべてのプロセッサで適度にコードが働くようにしたい場合の妥協案を示す。MOVZX EAX,BYTE PTR [M8]の代わりに以下のようにすればよい。
<PRE>
XOR EAX, EAX
MOV AL, BYTE PTR [M8]
</PRE>
<P>この組み合わせはPPro, PIIとPIIIで、後でEAXから読み出す場合にパーシャル・レジスタ・ストールを避ける特別な場合である。秘訣は、レジスタ自身でXORを取れば、レジスタには空というタグが付けられる所にある。プロセッサはEAXの上位24ビットがゼロであることを覚えており、パーシャル・ストールは避けることができる。この機構は一定の組み合わせの時のみ働く。
<PRE> XOR EAX, EAX
MOV AL, 3
MOV EBX, EAX ; ストールなし
XOR AH, AH
MOV AL, 3
MOV BX, AX ; ストールなし
XOR EAX, EAX
MOV AH, 3
MOV EBX, EAX ; ストールあり
SUB EBX, EBX
MOV BL, DL
MOV ECX, EBX ; ストールなし
MOV EBX, 0
MOV BL, DL
MOV ECX, EBX ; ストールあり
MOV BL, DL
XOR EBX, EBX ; ストールなし
</PRE>
<P>レジスタをゼロにするのに、レジスタそれ自身から減算することはXORを実行するのに等しいが、MOV命令を用いてゼロにした場合はストールを妨げることはできない。
<P>XORをループの外に置くことができる。
<PRE>
XOR EAX, EAX
MOV ECX, 100
LL: MOV AL, [ESI]
MOV [EDI], EAX ; ストールなし
INC ESI
ADD EDI, 4
DEC ECX
JNZ LL
</PRE>
<P>プロセッサは割り込み、分岐予測ミス、シリアル化イベントが起きない限り、EAXの上位24ビットがゼロであることを覚えている。
<P>レジスタ全体をプッシュするかもしれないサブルーチンを呼び出す前に、パーシャル・レジスタをすべて中和することを覚えておくべきである。
<PRE>
ADD BL, AL
MOV [MEM8], BL
XOR EBX, EBX ; BLを中和する
CALL _HighLevelFunction
</PRE>
<P>大部分の高級言語の手続きは、上で述べたようにBLレジスタを中和しておかないと、パーシャル・レジスタ・ストールを生じるようなEBXレジスタのプッシュを、手続きの最初で行う。
<P>レジスタをXORでゼロにする手法は、それより前の命令との依存性を断ち切ることはできない。
<PRE>
DIV EBX
MOV [MEM], EAX
MOV EAX, 0 ; 依存性を断ち切る
XOR EAX, EAX ; パーシャル・レジスタ・ストールを妨げる
MOV AL, CL
ADD EBX, EAX
</PRE>
<P>EAXを二回ゼロにすることは無駄に見えるかもしれないが、MOV EAX,0なしでは、最後の命令は遅いDIV命令が終わるまで待たなければならず、XOR EAX,EAXなしではパーシャル・レジスタ・ストールが生じる。
<P>FNSTSW AX命令は特別である。32ビットモードではEAX全体に書き込むように振る舞う。実際は、このようなことを32ビットモードで行っている:<BR>
<PRE>
AND EAX,0FFFF0000h / FNSTSW TEMP / OR EAX,TEMP
</PRE>
<P>これゆえ、32ビットモードではこの命令の後でEAXから読み出してもパーシャル・レジスタ・ストールは生じない。
<PRE>
FNSTSW AX / MOV EBX,EAX ; 16ビットモード時のみストールあり
MOV AX,0 / FNSTSW AX ; 32ビットモード時のみストールあり
</PRE>
<P>
<A name=19_2>
<H3>19.2 パーシャル・フラグ・ストール</H3>
フラグも同様パーシャル・レジスタ・ストールの原因となる。
<PRE>
CMP EAX, EBX
INC ECX
JBE XX ; パーシャル・フラグ・ストール
</PRE>
<P>JBE命令はキャリーフラグとゼロフラグの両方を読む。INC命令はゼロフラグを変化させるが、キャリーフラグを変化させない。JBE命令は、CMP命令からのキャリーフラグとINC命令からのゼロフラグを結合できるまで、前の二つの命令がリタイアするのを待たなければならない。この状態は意図的なフラグの組み合わせというよりはむしろバグである可能性が高い。これを修正するには、INC ECXをADD ECX,1に替えればよい。似たようなパーシャル・フラグ・ストールの原因となるバグは、SAHF / JL XXである。JL命令はサインフラグとオーバーフローフラグを調べるが、SAHF命令はオーバーフローフラグを変化させない。これを修正するには、JL XXをJS XXに替えればよい。
<P>思いがけいことに(インテルの説明書で述べられていることに反して)、フラグビットのいくつかを変更する命令の後で、変更しなかったフラグビットだけを読んだ時に、パーシャル・フラグ・ストールを受けることがある。
<PRE>
CMP EAX, EBX
INC ECX