HP OpenVMS MACRO コンパイラポーティングおよびユーザーズ・ガイド
HP OpenVMS MACRO コンパイラ ポーティングおよびユーザーズ・ガイド
第 4 章 移植したコードの性能改善
この章では,移植したコードの性能を改善する方法について説明します。
この章のトピックは以下のとおりです。
アラインされていないデータ参照は, OpenVMS Alpha や OpenVMS I64 でも動作しますが,低速です。システムは,アラインされていないアドレスのフォルトを取得して,アラインされていない参照を完了させる必要があるためです。データの参照がアラインされていないことが分かっている場合は,コンパイラは,手動でデータを抽出するための,アラインされていないクォドワードのロードとマスクを生成することができます。これはアラインされているロードより低速ですが,アラインメント・フォルトが発生する場合よりは高速です。ロングワードまたはクォドワードにアラインされていないグローバル・データ・ラベルに対しては,情報レベルのメッセージが表示されます。
また,/PRESERVE=ATOMICITY や .PRESERVE ATOMICITY を使用して,アラインされていないメモリ変更の参照を不可分にすることはできません。このような参照を行うと,回復不可能な予約オペランド・フォルトとなります。
4.1.1 アラインメントの想定 | |
デフォルトでは,コンパイラは以下のことを想定します。
- ベース・ポインタとして使用されるレジスタ内のアドレスは,ルーチンの入口でロングワードにアラインされている。
- 外部参照はロングワードにアラインされている。
- DIVL などの特定の種類の命令から得られたアドレスはアラインされていない。
コンパイラは,レジスタが変更されるたびに,レジスタ内のベース・アドレスがまだアラインされているかどうかを確認します。レジスタと指定されたオフセットでアラインされたアドレスになる場合は,コンパイラはメモリ参照に対してアラインされたロードまたは格納を使用します。コンパイラは,レジスタの使用を追跡し,ベース・アドレスがアラインされているかどうかを確認します。たとえば, MOVL 4(R1),R0 などで,格納されているメモリ・アドレスをロードするときや, MOVL @4(R1),R0 などで間接的に使用される場合は,コンパイラはアドレスがアラインされていると想定します。
MOVQ 命令などのクォドワード・メモリ参照では,レジスタ追跡コードによってアドレスがロングワードにアラインされていない可能性があることが分かっていないかぎり,コンパイラはベース・アドレスがクォドワードにアラインされているものと想定します。言い換えれば,クォドワードのレジスタ・アラインメントは追跡されません。ロングワードのアラインメントだけが追跡されます。
次の例のような, OpenVMS Alpha または OpenVMS I64 のビルトインでのクォドワードの参照は,新しいコードなので,アラインメントは正しいはずです。そのため,次の例のすべてのメモリ参照では,アラインされたクォドワードのロードと格納が使用されます。
EVAX_LDQ R1, (R2)
EVAX_ADDQ (R1), #1, (R3)
|
OpenVMS Alpha または OpenVMS I64 のビルトイン (EVAX_LDQU と EVAX_STQU 以外) を,クォドワードにアラインされていないアドレスに対して使用すると,実行時にアラインメント・フォルトが発生します。
4.1.2 アラインメントの想定を変更する指示文と修飾子 | |
コンパイラには,コンパイラのアラインメントの想定を変更するための, 2 つの指示文と 1 つの修飾子があります。どちらの指示文を指定しても,コンパイラはより効率の良いコードを生成できます。 .SET_REGISTERS 指示文では,レジスタがアラインされているかそうでないかを指定できます。この指示文は,演算の結果が,コンパイラが想定するものと逆である場合に使用します。また,指示文を使用しないとコンパイラが入力レジスタや出力レジスタとして検出しないレジスタを宣言することもできます。
.SYMBOL_ALIGNMENT 指示文では,シンボリック・オフセットを使用するメモリ参照のアラインメントを指定することができます。この指示文は,シンボリック・オフセットが使用されているすべての箇所でデータがアラインされていることが分かっている場合に使用します。
これらの指示文の説明は, 付録 B にあります。それぞれの説明の例で使用方法を示しています。
MACRO/MIGRATION コマンドに対する /UNALIGN 修飾子は,コンパイラに対し,アラインメントを追跡せずに,すべてのレジスタ・ベースのメモリ参照がアラインされていないものと想定するように指示します。これは,コンパイラがアラインメントを知っているスタック・ベースの参照や静的な参照には影響を与えません。
この修飾子の説明は 付録 A にあります。
4.1.3 アラインメント制御の優先順位 | |
コンパイラのアラインメント制御の優先順位を,優先順位の最も高いもの (.SYMBOL_ALIGNMENT) から最も低いもの (組み込みの想定と追跡メカニズム) の順に並べると次のようになります。
- .SYMBOL_ALIGNMENT 指示文
- .SET_REGISTER 指示文
- /UNALIGN 修飾子
- 組み込みの想定と追跡メカニズム
4.1.4 データのアラインメントでの推奨事項 | |
データのアラインメントでは次の推奨事項に従ってください。
- /PRESERVE=ATOMICITY または .PRESERVE ATOMICITY を使用してデータの参照を不可分にする必要がある場合は,データはアラインされている必要があります。
- パブリック・インタフェースでのアラインメントの問題は修正しないでください。既存のプログラムが正常に動作しなくなるおそれがあります。
- 内部インタフェースまたは特権インタフェースのデータでは,データのアラインメントを改善するための変更を自動的に行わないでください。データ構造にアクセスする頻度,構造をアラインし直すのに要する作業量,誤りが発生する危険性を考慮してください。必要な作業量を判断する際には,単に推測するのではなく,データに対するすべてのアクセスを確認してください。担当しているコードのすべてのアクセスを把握しており,いずれにしてもモジュールを変更する予定である場合は,アラインメントの問題を修正しても安全です。
- バイトまたはワードのデータを機械的にロングワードまたはクォドワードにアンパックしないでください。このような修正を行うのは,前述の注意および制約に従って,アラインメントの問題 (ワード境界にないワード) を修正する場合や,データの細分性に問題があることが分かっている場合です。
- データに対するすべてのアクセスを把握していない場合でも,アラインメントを修正するのが適切であるような状況があります。データに頻繁にアクセスする場合で,性能が問題となっており,データ構造を見直すことが不可避な場合は,同時に構造をアラインすることに意味があります。
作成しているコードに影響がある他のプログラマにも通知することが重要です。関連するすべてのモジュールが再コンパイルされるものと想定したり,データ・セルの分離に対する想定が誤っていることを,他の人がプログラムのドキュメントからを見つけてくれるとは思わないでください。このような変更によって,不適切なプログラミングが露呈したり,変更が順調に進まないことがあるということを常に想定してください。
Alpha アーキテクチャと Itanium アーキテクチャはパイプライン化されており,現在の命令が完了する前に,それよりも先のいくつかの命令を実行し始めます。パイプラインに命令が満たされた状態を保つようにコードを調整することで,はるかに高速にコードを動作させることができます。
Alpha アーキテクチャと Itanium アーキテクチャでは,次に実行する命令が命令パイプラインに正しく満たされるように,それぞれの条件分岐で分岐が発生するかどうかを予測しようとします。 Alpha アーキテクチャでは,前方条件分岐は成立せず,後方条件分岐が成立するものと予測されます。 Itanium アーキテクチャでは,分岐命令に分岐予測ヒントが埋め込まれています。分岐予測が外れると,パイプラインをフラッシュしなくてはならない上に,分岐先の命令が命令キャッシュにない可能性があるため,余分に時間がかかります。
コンパイラは,パイプライン化された Alpha アーキテクチャと Itanium アーキテクチャでコードを高い効率で実行できるように, VAX MACRO コードのフローに従って,最もよく実行されるコード・パスが連続したブロックになるような Alpha および Itanium のコードを生成しようとします。しかし,場合によっては,コンパイラのデフォルトの規則では最も効率の良いコードが生成されないことがあります。高い性能が要求されるコード・セクションでは,実行される可能性がもっと高いコード・パスをコンパイラに教えることで,生成されるコードの効率を高めることができます。
4.2.1 デフォルトのコード・フローと分岐予測 | |
特に指定しないかぎり,コンパイラは通常, VAX MACRO の無条件分岐をたどり,VAX MACRO の条件分岐は不成立とする経路で Alpha と Itanium のコードを生成します。たとえば,次の VAX MACRO のコード・シーケンスを考えます。
(Code block A)
BLBS R0,10$
(Code block B)
10$:
(Code block C)
BRB 30$
20$:
.
(Code block D)
.
30$:
(Code block E)
|
このシーケンスに対して生成される Alpha コードは,次のようになります。
(Code block A)
BLBS R0,10$
(Code block B)
10$:
(Code block C)
30$:
(Code block E)
|
コンパイラは BLBS 命令を不成立とし, BLBS の直後の命令から続行している点に注意してください。コンパイラは,BRB 命令で,分岐命令は全く生成せず,コード・ブロック C から生成された Alpha と Itanium のコードの後に,分岐先のコード・ブロック E から生成された Alpha と Itanium のコードを続けています。ラベル 20$ にあるコード・ブロック D のコードは,ルーチンの後のほうで生成されます。ラベル 20$ への分岐がない場合は,コンパイラは次の情報メッセージを出力し,コード・ブロック D に対する Alpha または Itanium のコードを生成しません。
UNRCHCODE, unreachable code
|
ほとんどの場合,このアルゴリズムによって,アーキテクチャの想定に合った Alpha と Itanium のコードが生成されます。
- VAX MACRO コードの条件分岐が後方分岐の場合は,分岐先はすでに Alpha および Itanium のコードが生成されている可能性が高いため,生成される分岐も後方となります。
- VAX MACRO コードの条件分岐が前方分岐の場合は,分岐先は Alpha および Itanium のコードがまだ生成されていない可能性が高いため,生成される分岐も前方となります。
ただし,コンパイラは無条件分岐をたどるため, VAX MACRO の後方分岐の分岐先はまだ生成されていない可能性があります。この場合,VAX MACRO のソース・コードで後方分岐だった条件分岐が,生成される Alpha および Itanium のコードでは前方分岐になることがあります。この問題の詳細と対処方法については 第 4.2.5 項 を参照してください。
前方分岐が成立するとコンパイラが想定するケースがあります。たとえば,次の一般的な VAX MACRO のコーディング・スタイルを考えます。
JSB XYZ ;Call a routine
BLBS R0,10$ ;Branch to continue on success
BRW ERROR ;Destination too far for byte offset
10$:
|
このように,分岐に続くインライン・コードが数行しかなく,分岐先でコード・フローに合流しない場合は,前方分岐を成立と判断します。これにより, OpenVMS Alpha システムと OpenVMS I64 システムでの分岐予測ミスによる遅延をなくすことができます。コンパイラは,分岐の見方を自動的に変更し,分岐とラベルの間のコードを,ルーチンの通常の出口の先に移動させます。この例に対しては,次のコードが生成されます。
JSR XYZ
BLBC $L1
10$:
.
.
.
(routine exit)
$L1: BRW ERROR
|
4.2.2 コンパイラの分岐予測の変更 | |
コンパイラでは 2 つの指示文 .BRANCH_LIKELY と .BRANCH_UNLIKELY が提供され,分岐予測に関する想定を変更することができます。指示文 .BRANCH_LIKELY は,分岐の可能性が高い (たとえば 75 パーセント以上) 場合に前方条件分岐に対して使用します。指示文 .BRANCH_UNLIKELY は,分岐の可能性が低い (たとえば 25 パーセント未満) 場合に後方条件分岐に対して使用します。
これらの指示文は,高い性能が要求されるコードでのみ使用してください。また,.BRANCH_UNLIKELY を追加すると,分岐が実際に成立した場合のパスに分岐命令が追加されるため,この指示文を追加する場合には特に注意してください。つまり,後方分岐が分岐命令の前方分岐に変更され,そこからさらに元の分岐先に分岐します。
無条件分岐をたどらないようにコンパイラに指示する指示文はありません。ただし,分岐をたどらないコードをコンパイラに生成させたい場合は,無条件分岐を,常に成立することが分かっている条件分岐に変更することができます。たとえば,現在のコード・セクションで,R3 に常にデータ構造のアドレスが格納されていることが分かっている場合は, BRB 命令を TSTL R3 に変更し,その後に BNEQ 命令を続けます。この分岐は常に成立しますが,コンパイラはそのまま次に進み,次の命令のコード生成を続行します。これを実行すると,常に分岐予測ミスが発生しますが,場合によっては有効です。
4.2.3 .BRANCH_LIKELY の使用方法 | |
成立する可能性が非常に高い前方条件分岐がコードにある場合は,分岐命令の直前に .BRANCH_LIKELY 指示文を挿入することで,その想定を使用してコードを生成するようにコンパイラに指示することができます。例を示します。
MOVL (R0),R1 ; Get structure
.BRANCH_LIKELY
BNEQ 10$ ; Structure exists
.
(Code to deal with missing structure, which is too large for
the compiler to automatically change the branch prediction)
.
10$:
|
コンパイラは分岐をたどり,前の例で説明したように,足りない構造を扱うすべてのコードをモジュールの最後に移動させることで,コードのフローを変更します。
4.2.4 .BRANCH_UNLIKELY の使用方法 | |
成立する可能性が非常に低い後方条件分岐がコードにある場合は,分岐命令の直前に .BRANCH_UNLIKELY 指示文を挿入することで,その想定を使用してコードを生成するようにコンパイラに指示することができます。例を示します。
MOVL #QUEUE,R0 ;Get queue header
10$: MOVL (R0),R0 ;Get entry from queue
BEQL 20$ ;Forward branch assumed unlikely
. ;by default
. ;Process queue entry
.
TSTL (R0) ;More than one entry (known to be
.BRANCH_UNLIKELY ;unlikely)
BNEQ 10$ ;This branch made into forward
20$: ;conditional branch
|
ここで .BRANCH_UNLIKELY 指示文を使用している理由は,コンパイラが 10$ への後方分岐を成立する可能性が高いと予測するためです。プログラマは,それがまれなケースであることを知っているため,分岐命令を,成立と予測されない前方分岐に変更するために指示文を使用しています。
前方分岐の分岐先に無条件分岐命令があり,その分岐命令で元の分岐先に戻ります。この場合も,このコードは,ルーチンの通常の出口の後に移動されます。上の VAX MACRO コードから生成されるコードは,次のようになります。
LDQ R0, 48(R27) ;Get address of QUEUE from linkage sect.
10$: LDL R0, (R0) ;Get entry from QUEUE
BEQ R0, 20$
.
. ;Process queue entry
.
LDL R22, (R0) ;Load temporary register with (R0)
BNE R22,$L1 ;Conditional forward branch predicted
20$: ;not taken by Alpha hardware
.
.
.
(routine exit)
$L1: BR 10$ ;Branch to original destination
|
4.2.5 ループへの前方ジャンプ | |
コンパイラがコード・フローをたどる方式であることが原因となり,ループの中に前方分岐した場合は,効率の良いコードにコンパイルされないケースあります。通常,この場合には,ループが 2 つの部分に大きく分割されたコードが生成されます。たとえば,次のマクロ・コーディング構造を考えます。
(Allocate a data block and set up initial pointer)
BRB 20$
10$: (Move block pointer to next section to be moved)
20$: (Move block of data)
(Test - is there more to move?)
(Yes, branch to 10$)
(Remainder of routine)
|
MACRO コンパイラは,コード・フローを生成する際に BRB 命令をたどり,その後の 10$ への条件分岐にたどり着きます。しかし,10$ のコードは,BRB 命令によってスキップされるため,ルーチンの最後になるまで生成されません。そのため,条件分岐は後方分岐ではなく前方分岐に変換されます。生成されるコードの配置は次のようになります。
(Allocate a data block and set up initial pointer)
20$: (Move block of data)
(Test - is there more to move?)
(Yes, branch to 10$)
.
.
(Remainder of routine)
(Routine exit)
.
.
10$: (Move block pointer to next section to be moved)
BRB 20$
|
その結果,10$ への分岐が常に不成立と予測され,コード・フローは 2 つの箇所の間で常に行ったり来たりするため,ループは非常に遅くなります。この状況は,10$ に戻る条件分岐の前に .BRANCH_LIKELY 指示文を挿入することで解消できます。これにより,次のようなコード・フローとなります。
(Allocate a data block and set up initial pointer)
20$: (Move block of data)
(Test - is there more to move?)
(No, branch to $L1)
10$: (Move block pointer to next section to be moved)
BRB 20$
$L1:
(Remainder of routine)
|
|