[インデックス 19304] ファイルの概要
このコミットは、Goコンパイラのレジスタ割り当てロジックにおけるバグ修正に関するものです。具体的には、レジスタアロケータがNOP
(No Operation)命令内で使用されている変数に対して誤って「クレジット」を与えていた問題を修正します。NOP
命令は最終的にコンパイル時に削除されるため、その中で変数が使用されてもレジスタ割り当ての考慮対象とすべきではない、という点が変更の核心です。
コミット
commit 1848d71445adc043b8c70cf80a36819b4b84efbd
Author: Josh Bleecher Snyder <josharian@gmail.com>
Date: Fri May 9 09:55:17 2014 -0700
cmd/gc: don't give credit for NOPs during register allocation
The register allocator decides which variables should be placed into registers by charging for each load/store and crediting for each use, and then selecting an allocation with minimal cost. NOPs will be eliminated, however, so using a variable in a NOP should not generate credit.
Issue 7867 arises from attempted registerization of multi-word variables because they are used in NOPs. By not crediting for that use, they will no longer be considered for registerization.
This fix could theoretically lead to better register allocation, but NOPs are rare relative to other instructions.
Fixes #7867.
LGTM=rsc
R=rsc
CC=golang-codereviews
https://golang.org/cl/94810044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1848d71445adc043b8c70cf80a36819b4b84efbd
元コミット内容
Goコンパイラ(cmd/gc
)において、レジスタアロケータがNOP
命令内で使用される変数に対してレジスタ割り当ての「クレジット」を与えないようにする変更です。レジスタアロケータは、各ロード/ストア操作に対してコストを課し、各使用に対してクレジットを与えることで、最小コストの割り当てを選択します。しかし、NOP
命令は最終的に削除されるため、その中での変数の使用はクレジットを生成すべきではありません。
この問題は、Issue 7867として報告されており、NOP
内で使用される多ワード変数(multi-word variables)が誤ってレジスタ化の対象となることが原因でした。この修正により、NOP
内での使用に対してクレジットが与えられなくなるため、これらの変数が不適切にレジスタ化されることがなくなります。
この修正は理論的にはより良いレジスタ割り当てにつながる可能性がありますが、NOP
命令は他の命令に比べて稀であるため、その影響は限定的であるとされています。
変更の背景
この変更は、Goコンパイラのレジスタ割り当てにおける特定のバグ、具体的にはIssue 7867を修正するために行われました。
レジスタアロケータは、プログラムの実行速度を向上させるために、頻繁にアクセスされる変数をCPUのレジスタに配置しようとします。このプロセスは、変数の使用頻度やアクセスパターンに基づいて「コスト」と「クレジット」を計算し、最適なレジスタ割り当てを決定します。
しかし、Goコンパイラのある段階で生成されるNOP
(No Operation)命令は、何もしない命令であり、最終的にはコンパイル時に削除されます。本来、このような削除される命令内で変数が使用されても、それは実際のプログラムの実行には影響しないため、レジスタ割り当ての判断材料とすべきではありませんでした。
Issue 7867では、特に多ワード変数(例えば、構造体や配列など、複数のCPUワードを占める変数)がNOP
命令内で使用されている場合に、レジスタアロケータがその使用を誤って「有効な使用」とみなし、レジスタ化の対象としてしまう問題が報告されました。これにより、本来レジスタに割り当てる必要のない変数がレジスタに割り当てられ、レジスタ資源の無駄遣いや、場合によってはコンパイルエラーを引き起こす可能性がありました。
このコミットは、この誤ったクレジット付与を排除することで、レジスタアロケータがより正確な判断を下し、コンパイラの堅牢性と生成されるコードの効率性を向上させることを目的としています。
前提知識の解説
1. コンパイラのレジスタ割り当て (Register Allocation)
レジスタ割り当ては、コンパイラの最適化フェーズにおける重要なステップの一つです。CPUのレジスタは非常に高速なメモリであり、プログラムの実行速度に直接影響します。レジスタ割り当ての目的は、プログラム内で頻繁に使用される変数(ローカル変数など)を、メモリ(スタックやヒープ)ではなくCPUのレジスタに配置することで、メモリアクセスのオーバーヘッドを減らし、プログラムの実行を高速化することです。
レジスタ割り当ては通常、グラフ彩色問題としてモデル化されます。変数の「ライブネス」(変数がその時点で有効である期間)を分析し、同時にライブである変数同士が同じレジスタを共有しないようにレジスタを割り当てます。
このコミットで言及されている「クレジット」と「コスト」は、レジスタ割り当てアルゴリズムにおけるヒューリスティックの一部です。
- コスト (Cost): 変数をレジスタに割り当てずにメモリに置いた場合に発生するオーバーヘッド(例: ロード/ストア命令の実行時間)を数値化したものです。
- クレジット (Credit): 変数をレジスタに割り当てることで得られる利益(例: メモリアクセスが不要になることによる高速化)を数値化したものです。
レジスタアロケータは、これらのコストとクレジットを比較し、全体として最も効率的なレジスタ割り当て(つまり、コストを最小化し、クレジットを最大化する割り当て)を見つけようとします。
2. NOP命令 (No Operation Instruction)
NOP
は「No Operation」の略で、文字通り「何もしない」命令です。CPUがこの命令を実行しても、レジスタの状態やメモリの内容は変化せず、プログラムカウンタが次の命令に進むだけです。
NOP
命令は様々な目的でコンパイラやアセンブラによって生成されます。
- アラインメント: コードやデータの特定のアドレス境界に合わせるために、パディングとして使用されます。
- タイミング調整: 非常に短い遅延を導入するために使用されることがあります(ただし、現代のCPUではパイプライン処理やキャッシュの影響で予測が難しい)。
- デバッグ: ブレークポイントを設定するためのプレースホルダーとして使用されることがあります。
- コード生成の簡略化: コンパイラが特定の最適化を行う際に、一時的に不要になった命令の代わりに
NOP
を挿入し、後で削除することがあります。
このコミットの文脈では、Goコンパイラがコード生成の過程で一時的にNOP
命令を生成し、そのNOP
命令内で変数が参照されることがあった、という点が重要です。これらのNOP
命令は最終的なバイナリコードには含まれず、コンパイルの最終段階で削除されます。
3. 多ワード変数 (Multi-word Variables)
Go言語において、int
やfloat64
のようなプリミティブ型は通常1ワード(CPUのアーキテクチャに依存しますが、32ビットシステムでは4バイト、64ビットシステムでは8バイト)で表現されます。しかし、struct
(構造体)やarray
(配列)のような複合型は、複数のワードを占めることがあります。これらを「多ワード変数」と呼びます。
多ワード変数をレジスタに割り当てることは、単一ワードの変数を割り当てるよりも複雑です。なぜなら、CPUのレジスタは通常、単一ワードのデータを保持するように設計されているため、多ワード変数をレジスタに格納するには複数のレジスタを使用するか、特別な命令が必要になる場合があるからです。そのため、多ワード変数のレジスタ化は、コンパイラにとってより慎重な判断が求められる領域です。
技術的詳細
このコミットの技術的詳細は、Goコンパイラのバックエンド、特にレジスタ割り当てフェーズにおけるpaint1
関数(src/cmd/5g/reg.c
, src/cmd/6g/reg.c
, src/cmd/8g/reg.c
に存在する)の挙動に焦点を当てています。これらのファイルは、それぞれ異なるアーキテクチャ(5gはARM、6gはx86-64、8gはx86)向けのGoコンパイラのレジスタ割り当てロジックを扱っています。
レジスタアロケータは、変数の使用(use1
, use2
)や設定(set
)に基づいて「クレジット」を計算します。このクレジットは、変数をレジスタに配置することの「価値」を示すものです。クレジットが高い変数ほど、レジスタに割り当てられる可能性が高まります。
問題は、コンパイラが生成する中間コードにおいて、ANOP
(Assembly NOP)命令内で変数が参照される場合があったことです。ANOP
命令は、最終的な機械語コード生成の前に削除されるため、その中での変数の使用は、実際のプログラムの実行には何ら寄与しません。しかし、既存のレジスタアロケータは、ANOP
命令内での変数の使用も「有効な使用」とみなし、クレジットを付与していました。
この誤ったクレジット付与により、本来レジスタに割り当てる必要のない変数(特に多ワード変数)が、NOP
命令内での参照によって不必要に高いクレジットを獲得し、レジスタ化の対象となってしまうことがありました。これは、レジスタ資源の非効率な利用につながるだけでなく、場合によってはコンパイルエラー(例: レジスタが不足する、多ワード変数をレジスタに収めるための複雑なコード生成が必要になるなど)を引き起こす可能性がありました。
この修正は、paint1
関数内でクレジットを計算する際に、現在の命令がANOP
であるかどうかをチェックする条件を追加することで、この問題を解決します。具体的には、r->f.prog->as != ANOP
という条件が追加され、命令がANOP
でない場合にのみ、変数の使用に対するクレジットが計算されるようになります。
これにより、レジスタアロケータは、実際に実行されるコード内での変数の使用のみを考慮してレジスタ割り当てを行うようになり、より正確で効率的なレジスタ割り当てが実現されます。コミットメッセージにあるように、NOP
命令は稀であるため、この修正が全体的なレジスタ割り当ての品質に与える影響は限定的かもしれませんが、特定のケースでのコンパイル問題の解決と、コンパイラのロジックの正確性向上に貢献します。
test/fixedbugs/issue7867.go
というテストファイルが追加されており、これはこの問題が特定の型の変数(complex64
, complex128
, struct{}
, string
, [4]byte
, []byte
, interface{}
, error
など)で発生していたことを示唆しています。これらの型は、多ワード変数であるか、あるいはレジスタ割り当てにおいて特別な考慮が必要な型です。テストコードは、これらの型がNOP
命令内で使用された場合に、コンパイルが失敗しないことを保証するために追加されました。
コアとなるコードの変更箇所
変更は主にGoコンパイラのレジスタ割り当てロジックを定義する以下の3つのファイルにわたっています。これらは異なるCPUアーキテクチャ(5g: ARM, 6g: x86-64, 8g: x86)に対応しています。
src/cmd/5g/reg.c
src/cmd/6g/reg.c
src/cmd/8g/reg.c
これらのファイル内のpaint1
関数において、変数の使用(use1
, use2
)や設定(set
)に基づいてchange
(クレジット)を計算する部分に、NOP
命令を無視するための条件が追加されています。
src/cmd/5g/reg.c
の変更点:
--- a/src/cmd/5g/reg.c
+++ b/src/cmd/5g/reg.c
@@ -1097,18 +1097,20 @@ paint1(Reg *r, int bn)
r->act.b[z] |= bb;
p = r->f.prog;
- if(r->use1.b[z] & bb) {
- change += CREF * r->f.loop;
- if(debug['R'] > 1)
- print("%d%P\tu1 %Q $%d\n", r->f.loop,
- p, blsh(bn), change);
- }
-
- if((r->use2.b[z]|r->set.b[z]) & bb) {
- change += CREF * r->f.loop;
- if(debug['R'] > 1)
- print("%d%P\tu2 %Q $%d\n", r->f.loop,
- p, blsh(bn), change);
+ if(r->f.prog->as != ANOP) { // don't give credit for NOPs
+ if(r->use1.b[z] & bb) {
+ change += CREF * r->f.loop;
+ if(debug['R'] > 1)
+ print("%d%P\tu1 %Q $%d\n", r->f.loop,
+ p, blsh(bn), change);
+ }
+ if((r->use2.b[z]|r->set.b[z]) & bb) {
+ change += CREF * r->f.loop;
+ if(debug['R'] > 1)
+ print("%d%P\tu2 %Q $%d\n", r->f.loop,
+ p, blsh(bn), change);
+ }
}
if(STORE(r) & r->regdiff.b[z] & bb) {
src/cmd/6g/reg.c
の変更点:
--- a/src/cmd/6g/reg.c
+++ b/src/cmd/6g/reg.c
@@ -942,12 +942,11 @@ paint1(Reg *r, int bn)
for(;;) {
r->act.b[z] |= bb;
- if(r->use1.b[z] & bb) {
- change += CREF * r->f.loop;
- }
-
- if((r->use2.b[z]|r->set.b[z]) & bb) {
- change += CREF * r->f.loop;
+ if(r->f.prog->as != ANOP) { // don't give credit for NOPs
+ if(r->use1.b[z] & bb)
+ change += CREF * r->f.loop;
+ if((r->use2.b[z]|r->set.b[z]) & bb)
+ change += CREF * r->f.loop;
}
if(STORE(r) & r->regdiff.b[z] & bb) {
src/cmd/8g/reg.c
の変更点:
--- a/src/cmd/8g/reg.c
+++ b/src/cmd/8g/reg.c
@@ -909,18 +909,19 @@ paint1(Reg *r, int bn)
r->act.b[z] |= bb;
p = r->f.prog;
- if(r->use1.b[z] & bb) {
- change += CREF * r->f.loop;
- if(p->as == AFMOVL || p->as == AFMOVW)
- if(BtoR(bb) != D_F0)
- change = -CINF;
- }
-
- if((r->use2.b[z]|r->set.b[z]) & bb) {
- change += CREF * r->f.loop;
- if(p->as == AFMOVL || p->as == AFMOVW)
- if(BtoR(bb) != D_F0)
- change = -CINF;
+ if(r->f.prog->as != ANOP) { // don't give credit for NOPs
+ if(r->use1.b[z] & bb) {
+ change += CREF * r->f.loop;
+ if(p->as == AFMOVL || p->as == AFMOVW)
+ if(BtoR(bb) != D_F0)
+ change = -CINF;
+ }
+ if((r->use2.b[z]|r->set.b[z]) & bb) {
+ change += CREF * r->f.loop;
+ if(p->as == AFMOVL || p->as == AFMOVW)
+ if(BtoR(bb) != D_F0)
+ change = -CINF;
+ }
}
if(STORE(r) & r->regdiff.b[z] & bb) {
また、この修正の動作を検証するための新しいテストファイルが追加されています。
test/fixedbugs/issue7867.go
このテストファイルは、様々なGoの型(特に多ワード型やインターフェース型)をNOP
命令が生成される可能性のあるコンテキストで使用し、コンパイルが成功することを確認します。
コアとなるコードの解説
変更の核心は、paint1
関数内のif(r->f.prog->as != ANOP)
という条件文の追加です。
r
:Reg
構造体へのポインタで、レジスタ割り当ての対象となる変数やレジスタに関する情報を含んでいます。r->f.prog
: 現在処理している命令(Prog
構造体)へのポインタです。r->f.prog->as
: 命令のオペレーションコード(アセンブリ命令の種類)を示します。ANOP
:No Operation
命令を表す定数です。
この条件文が追加される前は、r->use1.b[z] & bb
や (r->use2.b[z]|r->set.b[z]) & bb
といった変数の使用や設定をチェックするロジックが、命令の種類に関わらず実行され、change
変数にクレジットが加算されていました。
変更後は、このクレジット加算ロジック全体がif(r->f.prog->as != ANOP)
ブロックの内側に移動されました。これにより、現在の命令がANOP
(何もしない命令)である場合、その命令内で変数が使用されていても、レジスタ割り当てのクレジットは一切加算されなくなります。
この修正の意義は以下の通りです。
- 正確性の向上:
NOP
命令は最終的にコンパイル時に削除されるため、その中での変数の使用は実際の実行には影響しません。この修正により、レジスタアロケータは、実際に実行されるコード内での変数の使用のみを考慮するようになり、レジスタ割り当ての判断がより正確になります。 - リソースの効率化:
NOP
命令内での誤ったクレジット付与により、本来レジスタに割り当てる必要のない多ワード変数などがレジスタ化されることがありました。この修正により、不必要なレジスタ割り当てが減少し、レジスタ資源がより効率的に利用されるようになります。 - コンパイルの安定性: 特定のケースで発生していたコンパイルエラー(Issue 7867)が解消され、コンパイラの堅牢性が向上します。
この変更は、Goコンパイラのバックエンドにおける低レベルな最適化ロジックの改善であり、生成されるバイナリの品質とコンパイルプロセスの安定性に寄与します。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/1848d71445adc043b8c70cf80a36819b4b84efbd
- Go Issue 7867: https://github.com/golang/go/issues/7867
- Go Code Review 94810044: https://golang.org/cl/94810044
参考にした情報源リンク
- Goコンパイラのレジスタ割り当てに関する一般的な情報 (Goのドキュメントや関連する論文など)
- コンパイラの最適化に関する一般的な情報 (レジスタ割り当て、NOP命令など)
(注: 上記の「参考にした情報源リンク」は、この解説を生成する際に私が内部的に参照した一般的な知識領域を示しており、特定のURLを指すものではありません。もし特定の情報源が必要な場合は、別途Web検索を行う必要があります。)# [インデックス 19304] ファイルの概要
このコミットは、Goコンパイラのレジスタ割り当てロジックにおけるバグ修正に関するものです。具体的には、レジスタアロケータがNOP
(No Operation)命令内で使用されている変数に対して誤って「クレジット」を与えていた問題を修正します。NOP
命令は最終的にコンパイル時に削除されるため、その中で変数が使用されてもレジスタ割り当ての考慮対象とすべきではない、という点が変更の核心です。
コミット
commit 1848d71445adc043b8c70cf80a36819b4b84efbd
Author: Josh Bleecher Snyder <josharian@gmail.com>
Date: Fri May 9 09:55:17 2014 -0700
cmd/gc: don't give credit for NOPs during register allocation
The register allocator decides which variables should be placed into registers by charging for each load/store and crediting for each use, and then selecting an allocation with minimal cost. NOPs will be eliminated, however, so using a variable in a NOP should not generate credit.
Issue 7867 arises from attempted registerization of multi-word variables because they are used in NOPs. By not crediting for that use, they will no longer be considered for registerization.
This fix could theoretically lead to better register allocation, but NOPs are rare relative to other instructions.
Fixes #7867.
LGTM=rsc
R=rsc
CC=golang-codereviews
https://golang.org/cl/94810044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1848d71445adc043b8c70cf80a36819b4b84efbd
元コミット内容
Goコンパイラ(cmd/gc
)において、レジスタアロケータがNOP
命令内で使用される変数に対してレジスタ割り当ての「クレジット」を与えないようにする変更です。レジスタアロケータは、各ロード/ストア操作に対してコストを課し、各使用に対してクレジットを与えることで、最小コストの割り当てを選択します。しかし、NOP
命令は最終的に削除されるため、その中での変数の使用はクレジットを生成すべきではありません。
この問題は、Issue 7867として報告されており、NOP
内で使用される多ワード変数(multi-word variables)が誤ってレジスタ化の対象となることが原因でした。この修正により、NOP
内での使用に対してクレジットが与えられなくなるため、これらの変数が不適切にレジスタ化されることがなくなります。
この修正は理論的にはより良いレジスタ割り当てにつながる可能性がありますが、NOP
命令は他の命令に比べて稀であるため、その影響は限定的であるとされています。
変更の背景
この変更は、Goコンパイラのレジスタ割り当てにおける特定のバグ、具体的にはIssue 7867を修正するために行われました。
レジスタアロケータは、プログラムの実行速度を向上させるために、頻繁にアクセスされる変数をCPUのレジスタに配置しようとします。このプロセスは、変数の使用頻度やアクセスパターンに基づいて「コスト」と「クレジット」を計算し、最適なレジスタ割り当てを決定します。
しかし、Goコンパイラのある段階で生成されるNOP
(No Operation)命令は、何もしない命令であり、最終的にはコンパイル時に削除されます。本来、このような削除される命令内で変数が使用されても、それは実際のプログラムの実行には影響しないため、レジスタ割り当ての判断材料とすべきではありませんでした。
Issue 7867では、特に多ワード変数(例えば、構造体や配列など、複数のCPUワードを占める変数)がNOP
命令内で使用されている場合に、レジスタアロケータがその使用を誤って「有効な使用」とみなし、レジスタ化の対象としてしまう問題が報告されました。これにより、本来レジスタに割り当てる必要のない変数がレジスタに割り当てられ、レジスタ資源の無駄遣いや、場合によってはコンパイルエラーを引き起こす可能性がありました。
このコミットは、この誤ったクレジット付与を排除することで、レジスタアロケータがより正確な判断を下し、コンパイラの堅牢性と生成されるコードの効率性を向上させることを目的としています。
前提知識の解説
1. コンパイラのレジスタ割り当て (Register Allocation)
レジスタ割り当ては、コンパイラの最適化フェーズにおける重要なステップの一つです。CPUのレジスタは非常に高速なメモリであり、プログラムの実行速度に直接影響します。レジスタ割り当ての目的は、プログラム内で頻繁に使用される変数(ローカル変数など)を、メモリ(スタックやヒープ)ではなくCPUのレジスタに配置することで、メモリアクセスのオーバーヘッドを減らし、プログラムの実行を高速化することです。
レジスタ割り当ては通常、グラフ彩色問題としてモデル化されます。変数の「ライブネス」(変数がその時点で有効である期間)を分析し、同時にライブである変数同士が同じレジスタを共有しないようにレジスタを割り当てます。
このコミットで言及されている「クレジット」と「コスト」は、レジスタ割り当てアルゴリズムにおけるヒューリスティックの一部です。
- コスト (Cost): 変数をレジスタに割り当てずにメモリに置いた場合に発生するオーバーヘッド(例: ロード/ストア命令の実行時間)を数値化したものです。
- クレジット (Credit): 変数をレジスタに割り当てることで得られる利益(例: メモリアクセスが不要になることによる高速化)を数値化したものです。
レジスタアロケータは、これらのコストとクレジットを比較し、全体として最も効率的なレジスタ割り当て(つまり、コストを最小化し、クレジットを最大化する割り当て)を見つけようとします。
2. NOP命令 (No Operation Instruction)
NOP
は「No Operation」の略で、文字通り「何もしない」命令です。CPUがこの命令を実行しても、レジスタの状態やメモリの内容は変化せず、プログラムカウンタが次の命令に進むだけです。
NOP
命令は様々な目的でコンパイラやアセンブラによって生成されます。
- アラインメント: コードやデータの特定のアドレス境界に合わせるために、パディングとして使用されます。
- タイミング調整: 非常に短い遅延を導入するために使用されることがあります(ただし、現代のCPUではパイプライン処理やキャッシュの影響で予測が難しい)。
- デバッグ: ブレークポイントを設定するためのプレースホルダーとして使用されることがあります。
- コード生成の簡略化: コンパイラが特定の最適化を行う際に、一時的に不要になった命令の代わりに
NOP
を挿入し、後で削除することがあります。
このコミットの文脈では、Goコンパイラがコード生成の過程で一時的にNOP
命令を生成し、そのNOP
命令内で変数が参照されることがあった、という点が重要です。これらのNOP
命令は最終的なバイナリコードには含まれず、コンパイルの最終段階で削除されます。
3. 多ワード変数 (Multi-word Variables)
Go言語において、int
やfloat64
のようなプリミティブ型は通常1ワード(CPUのアーキテクチャに依存しますが、32ビットシステムでは4バイト、64ビットシステムでは8バイト)で表現されます。しかし、struct
(構造体)やarray
(配列)のような複合型は、複数のワードを占めることがあります。これらを「多ワード変数」と呼びます。
多ワード変数をレジスタに割り当てることは、単一ワードの変数を割り当てるよりも複雑です。なぜなら、CPUのレジスタは通常、単一ワードのデータを保持するように設計されているため、多ワード変数をレジスタに格納するには複数のレジスタを使用するか、特別な命令が必要になる場合があるからです。そのため、多ワード変数のレジスタ化は、コンパイラにとってより慎重な判断が求められる領域です。
技術的詳細
このコミットの技術的詳細は、Goコンパイラのバックエンド、特にレジスタ割り当てフェーズにおけるpaint1
関数(src/cmd/5g/reg.c
, src/cmd/6g/reg.c
, src/cmd/8g/reg.c
に存在する)の挙動に焦点を当てています。これらのファイルは、それぞれ異なるアーキテクチャ(5gはARM、6gはx86-64、8gはx86)向けのGoコンパイラのレジスタ割り当てロジックを扱っています。
レジスタアロケータは、変数の使用(use1
, use2
)や設定(set
)に基づいて「クレジット」を計算します。このクレジットは、変数をレジスタに配置することの「価値」を示すものです。クレジットが高い変数ほど、レジスタに割り当てられる可能性が高まります。
問題は、コンパイラが生成する中間コードにおいて、ANOP
(Assembly NOP)命令内で変数が参照される場合があったことです。ANOP
命令は、最終的な機械語コード生成の前に削除されるため、その中での変数の使用は、実際のプログラムの実行には何ら寄与しません。しかし、既存のレジスタアロケータは、ANOP
命令内での変数の使用も「有効な使用」とみなし、クレジットを付与していました。
この誤ったクレジット付与により、本来レジスタに割り当てる必要のない変数(特に多ワード変数)が、NOP
命令内での参照によって不必要に高いクレジットを獲得し、レジスタ化の対象となってしまうことがありました。これは、レジスタ資源の非効率な利用につながるだけでなく、場合によってはコンパイルエラー(例: レジスタが不足する、多ワード変数をレジスタに収めるための複雑なコード生成が必要になるなど)を引き起こす可能性がありました。
この修正は、paint1
関数内でクレジットを計算する際に、現在の命令がANOP
であるかどうかをチェックする条件を追加することで、この問題を解決します。具体的には、r->f.prog->as != ANOP
という条件が追加され、命令がANOP
でない場合にのみ、変数の使用に対するクレジットが計算されるようになります。
これにより、レジスタアロケータは、実際に実行されるコード内での変数の使用のみを考慮してレジスタ割り当てを行うようになり、より正確で効率的なレジスタ割り当てが実現されます。コミットメッセージにあるように、NOP
命令は稀であるため、この修正が全体的なレジスタ割り当ての品質に与える影響は限定的かもしれませんが、特定のケースでのコンパイル問題の解決と、コンパイラのロジックの正確性向上に貢献します。
test/fixedbugs/issue7867.go
というテストファイルが追加されており、これはこの問題が特定の型の変数(complex64
, complex128
, struct{}
, string
, [4]byte
, []byte
, interface{}
, error
など)で発生していたことを示唆しています。これらの型は、多ワード変数であるか、あるいはレジスタ割り当てにおいて特別な考慮が必要な型です。テストコードは、これらの型がNOP
命令内で使用された場合に、コンパイルが失敗しないことを保証するために追加されました。
コアとなるコードの変更箇所
変更は主にGoコンパイラのレジスタ割り当てロジックを定義する以下の3つのファイルにわたっています。これらは異なるCPUアーキテクチャ(5g: ARM, 6g: x86-64, 8g: x86)に対応しています。
src/cmd/5g/reg.c
src/cmd/6g/reg.c
src/cmd/8g/reg.c
これらのファイル内のpaint1
関数において、変数の使用(use1
, use2
)や設定(set
)に基づいてchange
(クレジット)を計算する部分に、NOP
命令を無視するための条件が追加されています。
src/cmd/5g/reg.c
の変更点:
--- a/src/cmd/5g/reg.c
+++ b/src/cmd/5g/reg.c
@@ -1097,18 +1097,20 @@ paint1(Reg *r, int bn)
r->act.b[z] |= bb;
p = r->f.prog;
- if(r->use1.b[z] & bb) {
- change += CREF * r->f.loop;
- if(debug['R'] > 1)
- print("%d%P\tu1 %Q $%d\n", r->f.loop,
- p, blsh(bn), change);
- }
-
- if((r->use2.b[z]|r->set.b[z]) & bb) {
- change += CREF * r->f.loop;
- if(debug['R'] > 1)
- print("%d%P\tu2 %Q $%d\n", r->f.loop,
- p, blsh(bn), change);
+ if(r->f.prog->as != ANOP) { // don't give credit for NOPs
+ if(r->use1.b[z] & bb) {
+ change += CREF * r->f.loop;
+ if(debug['R'] > 1)
+ print("%d%P\tu1 %Q $%d\n", r->f.loop,
+ p, blsh(bn), change);
+ }
+ if((r->use2.b[z]|r->set.b[z]) & bb) {
+ change += CREF * r->f.loop;
+ if(debug['R'] > 1)
+ print("%d%P\tu2 %Q $%d\n", r->f.loop,
+ p, blsh(bn), change);
+ }
}
if(STORE(r) & r->regdiff.b[z] & bb) {
src/cmd/6g/reg.c
の変更点:
--- a/src/cmd/6g/reg.c
+++ b/src/cmd/6g/reg.c
@@ -942,12 +942,11 @@ paint1(Reg *r, int bn)
for(;;) {
r->act.b[z] |= bb;
- if(r->use1.b[z] & bb) {
- change += CREF * r->f.loop;
- }
-
- if((r->use2.b[z]|r->set.b[z]) & bb) {
- change += CREF * r->f.loop;
+ if(r->f.prog->as != ANOP) { // don't give credit for NOPs
+ if(r->use1.b[z] & bb)
+ change += CREF * r->f.loop;
+ if((r->use2.b[z]|r->set.b[z]) & bb)
+ change += CREF * r->f.loop;
}
if(STORE(r) & r->regdiff.b[z] & bb) {
src/cmd/8g/reg.c
の変更点:
--- a/src/cmd/8g/reg.c
+++ b/src/cmd/8g/reg.c
@@ -909,18 +909,19 @@ paint1(Reg *r, int bn)
r->act.b[z] |= bb;
p = r->f.prog;
- if(r->use1.b[z] & bb) {
- change += CREF * r->f.loop;
- if(p->as == AFMOVL || p->as == AFMOVW)
- if(BtoR(bb) != D_F0)
- change = -CINF;
- }
-
- if((r->use2.b[z]|r->set.b[z]) & bb) {
- change += CREF * r->f.loop;
- if(p->as == AFMOVL || p->as == AFMOVW)
- if(BtoR(bb) != D_F0)
- change = -CINF;
+ if(r->f.prog->as != ANOP) { // don't give credit for NOPs
+ if(r->use1.b[z] & bb) {
+ change += CREF * r->f.loop;
+ if(p->as == AFMOVL || p->as == AFMOVW)
+ if(BtoR(bb) != D_F0)
+ change = -CINF;
+ }
+ if((r->use2.b[z]|r->set.b[z]) & bb) {
+ change += CREF * r->f.loop;
+ if(p->as == AFMOVL || p->as == AFMOVW)
+ if(BtoR(bb) != D_F0)
+ change = -CINF;
+ }
}
if(STORE(r) & r->regdiff.b[z] & bb) {
また、この修正の動作を検証するための新しいテストファイルが追加されています。
test/fixedbugs/issue7867.go
このテストファイルは、様々なGoの型(特に多ワード型やインターフェース型)をNOP
命令が生成される可能性のあるコンテキストで使用し、コンパイルが成功することを確認します。
コアとなるコードの解説
変更の核心は、paint1
関数内のif(r->f.prog->as != ANOP)
という条件文の追加です。
r
:Reg
構造体へのポインタで、レジスタ割り当ての対象となる変数やレジスタに関する情報を含んでいます。r->f.prog
: 現在処理している命令(Prog
構造体)へのポインタです。r->f.prog->as
: 命令のオペレーションコード(アセンブリ命令の種類)を示します。ANOP
:No Operation
命令を表す定数です。
この条件文が追加される前は、r->use1.b[z] & bb
や (r->use2.b[z]|r->set.b[z]) & bb
といった変数の使用や設定をチェックするロジックが、命令の種類に関わらず実行され、change
変数にクレジットが加算されていました。
変更後は、このクレジット加算ロジック全体がif(r->f.prog->as != ANOP)
ブロックの内側に移動されました。これにより、現在の命令がANOP
(何もしない命令)である場合、その命令内で変数が使用されていても、レジスタ割り当てのクレジットは一切加算されなくなります。
この修正の意義は以下の通りです。
- 正確性の向上:
NOP
命令は最終的にコンパイル時に削除されるため、その中での変数の使用は実際の実行には影響しません。この修正により、レジスタアロケータは、実際に実行されるコード内での変数の使用のみを考慮するようになり、レジスタ割り当ての判断がより正確になります。 - リソースの効率化:
NOP
命令内での誤ったクレジット付与により、本来レジスタに割り当てる必要のない多ワード変数などがレジスタ化されることがありました。この修正により、不必要なレジスタ割り当てが減少し、レジスタ資源がより効率的に利用されるようになります。 - コンパイルの安定性: 特定のケースで発生していたコンパイルエラー(Issue 7867)が解消され、コンパイラの堅牢性が向上します。
この変更は、Goコンパイラのバックエンドにおける低レベルな最適化ロジックの改善であり、生成されるバイナリの品質とコンパイルプロセスの安定性に寄与します。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/1848d71445adc043b8c70cf80a36819b4b84efbd
- Go Issue 7867: https://github.com/golang/go/issues/7867
- Go Code Review 94810044: https://golang.org/cl/94810044
参考にした情報源リンク
- Goコンパイラのレジスタ割り当てに関する一般的な情報 (Goのドキュメントや関連する論文など)
- コンパイラの最適化に関する一般的な情報 (レジスタ割り当て、NOP命令など)
(注: 上記の「参考にした情報源リンク」は、この解説を生成する際に私が内部的に参照した一般的な知識領域を示しており、特定のURLを指すものではありません。もし特定の情報源が必要な場合は、別途Web検索を行う必要があります。)