[インデックス 18482] ファイルの概要
このコミットは、GoコンパイラのARMアーキテクチャ向けバックエンドであるcmd/5g
におけるレジスタ最適化(regopt
)のバグ修正と、それに伴ういくつかの改善を目的としています。特に、コピー伝播(copyprop
)フェーズで発生していたレジスタタイプの誤認識が主要な問題でした。
コミット
commit 684332f47cff7a2aff1fddfe8b002ed07fb6e4a0
Author: Russ Cox <rsc@golang.org>
Date: Thu Feb 13 03:54:55 2014 +0000
cmd/5g: fix regopt bug in copyprop
copyau1 was assuming that it could deduce the type of the
middle register p->reg from the type of the left or right
argument: in CMPF F1, F2, the p->reg==2 must be a D_FREG
because p->from is F1, and in CMP R1, R2, the p->reg==2 must
be a D_REG because p->from is R1.
This heuristic fails for CMP $0, R2, which was causing copyau1
not to recognize p->reg==2 as a reference to R2, which was
keeping it from properly renaming the register use when
substituting registers.
cmd/5c has the right approach: look at the opcode p->as to
decide the kind of register. It is unclear where 5g's copyau1
came from; perhaps it was an attempt to avoid expanding 5c's
a2type to include new instructions used only by 5g.
Copy a2type from cmd/5c, expand to include additional instructions,
and make it crash the compiler if asked about an instruction
it does not understand (avoid silent bugs in the future if new
instructions are added).
Should fix current arm build breakage.
While we're here, fix the print statements dumping the pred and
succ info in the asm listing to pass an int arg to %.4ud
(Prog.pc is a vlong now, due to the liblink merge).
TBR=ken2
CC=golang-codereviews
https://golang.org/cl/62730043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/684332f47cff7a2aff1fddfe8b002ed07fb6e4a0
元コミット内容
このコミットは、GoコンパイラのARMアーキテクチャ向けバックエンドであるcmd/5g
におけるレジスタ最適化のバグを修正します。具体的には、コピー伝播(copyprop
)のフェーズで、copyau1
関数がレジスタの型を誤って推論していた問題に対処しています。
copyau1
は、命令の中央レジスタ(p->reg
)の型を、左または右の引数から推測しようとしていました。例えば、CMPF F1, F2
のような浮動小数点比較命令では、p->reg==2
がD_FREG
(浮動小数点レジスタ)であると推測し、CMP R1, R2
のような汎用レジスタ比較命令ではD_REG
(汎用レジスタ)であると推測していました。
しかし、このヒューリスティックはCMP $0, R2
(即値0とレジスタR2の比較)のような命令で破綻しました。この場合、copyau1
はp->reg==2
をR2
への参照として正しく認識できず、レジスタの置き換え時にレジスタ名の変更が適切に行われない原因となっていました。
この問題の解決策として、cmd/5c
(x86アーキテクチャ向けコンパイラ)が採用している、命令のオペコード(p->as
)に基づいてレジスタの型を決定するアプローチが導入されました。cmd/5g
のcopyau1
の起源は不明ですが、おそらくcmd/5c
のa2type
関数を、5g
のみで使用される新しい命令を含めるために拡張することを避ける試みだったのかもしれません。
修正内容は以下の通りです。
cmd/5c
からa2type
関数をコピーし、cmd/5g
に特有の追加命令をサポートするように拡張します。- 将来的なサイレントバグを防ぐため、
a2type
が理解できない命令に遭遇した場合にコンパイラをクラッシュさせる(fatal
エラーを発生させる)ように変更します。
また、このコミットには、liblink
のマージによりProg.pc
がvlong
型になったことに起因する、アセンブリリストのpred
(先行命令)およびsucc
(後続命令)情報をダンプする際のprint
文のバグ修正も含まれています。%.4ud
フォーマット指定子にint
型の引数を渡すように修正され、これにより現在のARMビルドの破損が解消されるはずです。
変更の背景
このコミットの背景には、Goコンパイラのレジスタ最適化における正確性の問題と、コンパイラの堅牢性の向上が挙げられます。
- レジスタ最適化のバグ:
cmd/5g
のコピー伝播フェーズにおけるレジスタタイプの誤認識は、コンパイラが生成するコードの正確性に直接影響を与えます。特にCMP $0, R2
のような一般的な命令で問題が発生することは、コンパイラの信頼性を損なう重大なバグです。レジスタの誤った認識は、不適切なレジスタ割り当てや、最適化の失敗、さらには誤ったコード生成につながる可能性があります。 - コンパイラ設計の一貫性:
cmd/5c
が既にオペコードに基づいてレジスタ型を決定する堅牢なメカニズム(a2type
)を持っていたにもかかわらず、cmd/5g
が異なる、かつ脆弱なヒューリスティックを使用していたことは、コンパイラバックエンド間の設計の一貫性の欠如を示唆しています。このコミットは、より良い設計パターンを共有することで、コンパイラ全体の品質を向上させます。 - 将来的なバグの防止:
a2type
が未知の命令に遭遇した場合にコンパイラをクラッシュさせるように変更することは、非常に重要な改善です。これにより、新しい命令が追加された際に、コンパイラがその命令を誤って処理したり、サイレントに間違ったコードを生成したりするリスクを低減します。明示的なクラッシュは、開発者が問題を早期に発見し、修正することを促します。 - ビルドの安定性:
Prog.pc
の型変更によるビルド破損は、コンパイラの開発プロセスにおける安定性の問題です。liblink
のマージという大きな変更に伴う副作用を修正することで、開発者がGoコンパイラをビルドし、テストする際の障壁を取り除きます。
これらの背景から、このコミットはGoコンパイラの正確性、堅牢性、そして開発の安定性を向上させるための重要な修正であると言えます。
前提知識の解説
このコミットを理解するためには、以下のGoコンパイラの内部構造と最適化に関する基本的な知識が必要です。
-
Goコンパイラの構造:
- Goコンパイラは、フロントエンド、ミドルエンド、バックエンドの3つの主要なフェーズに分かれています。
- フロントエンド: ソースコードの字句解析、構文解析、意味解析を行い、抽象構文木(AST)を生成します。
- ミドルエンド: ASTを中間表現(IR)に変換し、プラットフォーム非依存の最適化(例: インライン化、デッドコード削除)を行います。
- バックエンド: IRをターゲットアーキテクチャの機械語に変換し、プラットフォーム依存の最適化(例: レジスタ割り当て、命令スケジューリング)を行います。
cmd/5g
,cmd/6g
,cmd/8g
などは、それぞれARM (5g), x86-64 (6g), x86 (8g) などの異なるアーキテクチャ向けのバックエンドコンパイラを指します。
-
レジスタ最適化 (Register Optimization):
- コンパイラ最適化の一種で、プログラムの実行速度を向上させるために、CPUのレジスタを効率的に使用することを目的とします。
- レジスタはメモリよりもアクセスが高速であるため、頻繁に使用される値をレジスタに保持することでパフォーマンスが向上します。
- レジスタ割り当て(Register Allocation)は、変数をどのレジスタに割り当てるかを決定するプロセスです。
-
コピー伝播 (Copy Propagation):
- データフロー最適化の一種です。
x = y
のようなコピー命令がある場合、その後のコードでx
が使用されている箇所をy
に置き換えることで、コピー命令自体を削除したり、レジスタの使用を最適化したりします。- 例:
をa = b c = a + 1
に変換する。c = b + 1
- この最適化は、レジスタの再利用や、不要なデータ移動の削減に貢献します。
-
Prog
構造体とAdr
構造体:- Goコンパイラのバックエンドでは、プログラムは一連の命令(
Prog
構造体で表現されることが多い)として扱われます。 Prog
構造体は、命令のオペコード(as
フィールド)、ソースオペランド(from
フィールド)、デスティネーションオペランド(to
フィールド)、そして場合によっては中央レジスタ(reg
フィールド)などを含みます。Adr
構造体は、オペランド(アドレス)を表し、その型(type
フィールド、例:D_REG
,D_FREG
,D_NONE
)やレジスタ番号(reg
フィールド)などを含みます。p->as
: 命令のオペコード(例:AADD
,ACMP
,ACMPF
)。p->reg
: 命令の中央レジスタ。例えば、ADD R1, R2, R3
のような命令ではR2
が中央レジスタに該当します。p->from
: 命令のソースオペランド。p->to
: 命令のデスティネーションオペランド。
- Goコンパイラのバックエンドでは、プログラムは一連の命令(
-
レジスタの種類:
D_REG
: 汎用レジスタ(整数演算などに使用)。D_FREG
: 浮動小数点レジスタ(浮動小数点演算などに使用)。NREG
: 無効なレジスタ、またはレジスタが指定されていないことを示す。D_NONE
: オペランドが存在しないことを示す。
-
liblink
:- Goのリンカライブラリ。コンパイラが生成したオブジェクトファイルを結合して実行可能ファイルを生成します。
Prog.pc
(プログラムカウンタ)の型がvlong
(64ビット整数)に変更されたのは、このliblink
のマージによるものです。これは、より大きなプログラムや、アドレス空間が広いアーキテクチャに対応するためと考えられます。
これらの概念を理解することで、コミットが解決しようとしている問題と、その解決策の技術的な詳細をより深く把握できます。
技術的詳細
このコミットの技術的詳細は、主にcmd/5g/peep.c
におけるcopyau1
関数の修正と、新しいa2type
関数の導入に集約されます。
copyau1
のバグと修正
元のcopyau1
関数は、コピー伝播の際に命令の中央レジスタ(p->reg
)の型を推測する際に、その命令のソースオペランド(p->from
)やデスティネーションオペランド(p->to
)の型に依存していました。
例えば、CMPF F1, F2
(浮動小数点比較)のような命令では、p->from
が浮動小数点レジスタF1
であることから、p->reg
も浮動小数点レジスタであると推測していました。同様に、CMP R1, R2
(汎用レジスタ比較)では、p->from
が汎用レジスタR1
であることから、p->reg
も汎用レジスタであると推測していました。
このヒューリスティックは、多くのケースで機能しましたが、CMP $0, R2
のような命令で問題が発生しました。この命令では、p->from
は即値($0
)であり、レジスタではありません。そのため、copyau1
はp->from
からp->reg
の型を推測することができず、p->reg==2
がR2
への参照であることを正しく認識できませんでした。結果として、コピー伝播の際にレジスタの置き換え(リネーム)が適切に行われず、最適化が失敗したり、誤ったコードが生成されたりする可能性がありました。
修正後のcopyau1
は、この推測ロジックを完全に削除し、新しく導入されたa2type(p)
関数を使用してp->reg
の正しい型を取得するように変更されました。これにより、copyau1
は命令のオペコードに基づいてレジスタの型を正確に判断できるようになり、CMP $0, R2
のようなエッジケースでも正しく動作するようになりました。
a2type
関数の導入
a2type
関数は、cmd/5c
(x86コンパイラ)からコピーされ、cmd/5g
(ARMコンパイラ)の命令セットに合わせて拡張されました。この関数は、Prog
構造体(命令)を受け取り、その命令の中央レジスタ(p->reg
)がどのような型のレジスタであるべきかを返します。
a2type
の内部では、命令のオペコード(p->as
)に基づいてswitch
文が使用されます。
AAND
,AEOR
,ASUB
,AADD
,ACMP
などの汎用レジスタ演算命令に対してはD_REG
を返します。ACMPF
,ACMPD
,AADDF
,AADDD
などの浮動小数点レジスタ演算命令に対してはD_FREG
を返します。p->reg
がNREG
(レジスタが指定されていない)の場合はD_NONE
を返します。
最も重要な変更点は、default
ケースでfatal("a2type: unhandled %P", p)
を呼び出すようにしたことです。これは、a2type
が認識しない新しい命令が追加された場合に、コンパイラが即座にクラッシュするようにします。これにより、将来的に新しい命令が追加された際に、a2type
がその命令のレジスタ型を誤って推測したり、サイレントに間違ったコードを生成したりする「サイレントバグ」を防ぐことができます。コンパイラのクラッシュは、開発者に問題を明確に通知し、早期の修正を促します。
Prog.pc
の型変換とprint
文の修正
liblink
のマージにより、Prog
構造体のpc
フィールド(プログラムカウンタ、命令のアドレスを示す)の型がint
からvlong
(64ビット整数)に変更されました。しかし、src/cmd/5g/reg.c
, src/cmd/6g/reg.c
, src/cmd/8g/reg.c
内のdumpit
関数では、このpc
の値をprint
関数に%.4ud
というフォーマット指定子(符号なし10進整数)で渡していました。
%.4ud
はint
型の引数を期待するため、vlong
型の値をそのまま渡すと、コンパイラが警告を発したり、場合によっては未定義の動作を引き起こしたりする可能性があります。このコミットでは、print(" %.4ud", r1->prog->pc);
という行をprint(" %.4ud", (int)r1->prog->pc);
と修正し、明示的にvlong
型のpc
をint
型にキャストするようにしました。これにより、フォーマット指定子と引数の型が一致し、ARMビルドの破損が解消されました。これは、コンパイラのデバッグ出力の正確性と、ビルドの安定性を確保するための重要な修正です。
これらの変更は、Goコンパイラのバックエンドにおけるレジスタ最適化の正確性を向上させ、将来的なバグの発生を防ぐための堅牢なメカニズムを導入し、さらにビルドの安定性も確保しています。
コアとなるコードの変更箇所
このコミットのコアとなるコードの変更は、主にsrc/cmd/5g/peep.c
に集中しています。
src/cmd/5g/peep.c
a2type
関数の追加:--- a/src/cmd/5g/peep.c +++ b/src/cmd/5g/peep.c @@ -1242,35 +1242,79 @@ copyau(Adr *a, Adr *v) return 0; } +static int +a2type(Prog *p) +{ + if(p->reg == NREG) + return D_NONE; + + switch(p->as) { + default: + fatal("a2type: unhandled %P", p); + + case AAND: + case AEOR: + case ASUB: + case ARSB: + case AADD: + case AADC: + case ASBC: + case ARSC: + case ATST: + case ATEQ: + case ACMP: + case ACMN: + case AORR: + case ABIC: + case AMVN: + case ASRL: + case ASRA: + case ASLL: + case AMULU: + case ADIVU: + case AMUL: + case ADIV: + case AMOD: + case AMODU: + case AMULA: + case AMULL: + case AMULAL: + case AMULLU: + case AMULALU: + case AMULWT: + case AMULWB: + case AMULAWT: + case AMULAWB: + return D_REG; + + case ACMPF: + case ACMPD: + case AADDF: + case AADDD: + case ASUBF: + case ASUBD: + case AMULF: + case AMULD: + case ADIVF: + case ADIVD: + case ASQRTF: + case ASQRTD: + case AABSF: + case AABSD: + return D_FREG; + } +} +
copyau1
関数の修正:--- a/src/cmd/5g/peep.c +++ b/src/cmd/5g/peep.c @@ -1242,35 +1242,79 @@ copyau(Adr *a, Adr *v) return 0; } +... (a2type function added above) ... + /* * compare v to the center * register in p (p->reg) - * the trick is that this - * register might be D_REG - * D_FREG. there are basically - * two cases, - *\tADD r,r,r - *\tCMP r,r, */ static int copyau1(Prog *p, Adr *v) { - - if(regtyp(v)) - if(p->reg == v->reg) { - if(p->to.type != D_NONE) { - if(v->type == p->to.type) - return 1; - return 0; - } - if(p->from.type != D_NONE) { - if(v->type == p->from.type) - return 1; - return 0; - } - print("copyau1: can't tell %P\n", p); - } - return 0; + if(v->type == D_REG && v->reg == NREG) + return 0; + return p->reg == v->reg && a2type(p) == v->type; }
src/cmd/5g/reg.c
, src/cmd/6g/reg.c
, src/cmd/8g/reg.c
これらのファイルでは、dumpit
関数内のprint
文が修正されています。
例: src/cmd/5g/reg.c
--- a/src/cmd/5g/reg.c
+++ b/src/cmd/5g/reg.c
@@ -1291,9 +1291,9 @@ dumpit(char *str, Flow *r0, int isreg)
if(r1 != nil) {
print("\tpred:");
for(; r1 != nil; r1 = r1->p2link)
- print(" %.4ud", r1->prog->pc);
+ print(" %.4ud", (int)r1->prog->pc);
if(r->p1 != nil)
- print(" (and %.4ud)", r->p1->prog->pc);
+ print(" (and %.4ud)", (int)r->p1->prog->pc);
else
print(" (only)");
print("\n");
@@ -1302,7 +1302,7 @@ dumpit(char *str, Flow *r0, int isreg)
// if(r1 != nil) {
// print("\tsucc:");
// for(; r1 != R; r1 = r1->s1)
-// print(" %.4ud", r1->prog->pc);
+// print(" %.4ud", (int)r1->prog->pc);
// print("\n");
// }
}
同様の変更がsrc/cmd/6g/reg.c
とsrc/cmd/8g/reg.c
にも適用されています。
コアとなるコードの解説
src/cmd/5g/peep.c
-
a2type
関数の追加:- この関数は、Goコンパイラの命令(
Prog *p
)を受け取り、その命令の中央レジスタ(p->reg
)がどのような型のレジスタであるべきかを決定します。 if(p->reg == NREG) return D_NONE;
:p->reg
が有効なレジスタではない場合(NREG
は"No Register"を意味する定数)、レジスタ型はD_NONE
(なし)と判断されます。switch(p->as)
: 命令のオペコード(p->as
)に基づいて、レジスタの型を分岐します。case AAND: ... case AMULAWB:
: これらのオペコードは、汎用レジスタ(整数演算など)を使用する命令です。したがって、D_REG
(汎用レジスタ型)を返します。case ACMPF: ... case AABSD:
: これらのオペコードは、浮動小数点レジスタ(浮動小数点演算など)を使用する命令です。したがって、D_FREG
(浮動小数点レジスタ型)を返します。
default: fatal("a2type: unhandled %P", p);
: これが最も重要な変更点の一つです。もしa2type
が認識しないオペコードに遭遇した場合、コンパイラはfatal
エラーを発生させてクラッシュします。これにより、将来的に新しい命令が追加された際に、その命令のレジスタ型が未定義のまま処理されることによるサイレントバグを防ぎ、開発者に問題を早期に通知します。
- この関数は、Goコンパイラの命令(
-
copyau1
関数の修正:- この関数は、コピー伝播のロジックの一部であり、特定のレジスタ
v
が命令p
の中央レジスタp->reg
と一致するかどうか、およびその型が正しいかを比較します。 - 修正前: 複雑な条件分岐を使用して、
p->from
やp->to
の型からp->reg
の型を推測しようとしていました。この推測ロジックがCMP $0, R2
のような命令で失敗し、バグの原因となっていました。また、推測できない場合にprint("copyau1: can't tell %P\n", p);
という警告を出力していました。 - 修正後:
if(v->type == D_REG && v->reg == NREG) return 0;
: これは、v
が汎用レジスタ型でありながらレジスタ番号が指定されていない(無効な)場合は、比較対象ではないとして0
を返します。return p->reg == v->reg && a2type(p) == v->type;
: この行がバグ修正の核心です。p->reg == v->reg
: 命令の中央レジスタ番号が、比較対象のレジスタv
のレジスタ番号と一致するかどうかをチェックします。a2type(p) == v->type
: 新しく導入されたa2type
関数を呼び出し、命令p
の中央レジスタの正しい型を取得します。そして、その型が比較対象のレジスタv
の型と一致するかどうかをチェックします。
- この変更により、
copyau1
はレジスタの型を推測するのではなく、a2type
によって正確に決定された型を使用するようになり、コピー伝播の正確性が大幅に向上しました。
- この関数は、コピー伝播のロジックの一部であり、特定のレジスタ
src/cmd/5g/reg.c
, src/cmd/6g/reg.c
, src/cmd/8g/reg.c
- これらのファイルにある
dumpit
関数は、コンパイラのデバッグ出力(アセンブリリスト)の一部として、命令の先行(pred
)および後続(succ
)情報を表示するために使用されます。 print(" %.4ud", r1->prog->pc);
のような行が、print(" %.4ud", (int)r1->prog->pc);
に変更されています。- これは、
Prog.pc
フィールドの型がint
からvlong
(64ビット整数)に変更されたことによるものです。%.4ud
というフォーマット指定子は、符号なしの10進整数(通常は32ビットのint
)を期待します。vlong
をそのまま渡すと、コンパイラが警告を発したり、出力が不正になったりする可能性があります。 - 明示的に
(int)
にキャストすることで、vlong
の値をint
に切り捨てて(または変換して)print
関数に渡し、フォーマット指定子との型の一貫性を保ち、ビルドエラーや不正な出力を防ぎます。これは、デバッグ出力の正確性を維持し、コンパイラのビルドプロセスを安定させるための修正です。
これらの変更は、Goコンパイラのバックエンドにおけるレジスタ最適化の正確性を向上させ、将来的なバグの発生を防ぐための堅牢なメカニズムを導入し、さらにビルドの安定性も確保しています。
関連リンク
- Go言語の公式リポジトリ: https://github.com/golang/go
- このコミットのChange-ID: https://golang.org/cl/62730043 (GoのコードレビューシステムGerritへのリンク)
参考にした情報源リンク
- Go言語のコンパイラに関するドキュメントやソースコード(特に
src/cmd/5g/
,src/cmd/6g/
,src/cmd/8g/
ディレクトリ内のファイル)。 - コンパイラ最適化(レジスタ割り当て、コピー伝播など)に関する一般的な情報源。
- Go言語の
liblink
に関する情報。 - Go言語の
Prog
およびAdr
構造体に関する定義(通常はsrc/cmd/internal/obj/
や各アーキテクチャのobj
パッケージ内)。 - Go言語の
fatal
関数に関する情報(通常はコンパイラのユーティリティ関数として定義されている)。 - C言語の
printf
フォーマット指定子に関する情報。