Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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==2D_FREG(浮動小数点レジスタ)であると推測し、CMP R1, R2のような汎用レジスタ比較命令ではD_REG(汎用レジスタ)であると推測していました。

しかし、このヒューリスティックはCMP $0, R2(即値0とレジスタR2の比較)のような命令で破綻しました。この場合、copyau1p->reg==2R2への参照として正しく認識できず、レジスタの置き換え時にレジスタ名の変更が適切に行われない原因となっていました。

この問題の解決策として、cmd/5c(x86アーキテクチャ向けコンパイラ)が採用している、命令のオペコード(p->as)に基づいてレジスタの型を決定するアプローチが導入されました。cmd/5gcopyau1の起源は不明ですが、おそらくcmd/5ca2type関数を、5gのみで使用される新しい命令を含めるために拡張することを避ける試みだったのかもしれません。

修正内容は以下の通りです。

  1. cmd/5cからa2type関数をコピーし、cmd/5gに特有の追加命令をサポートするように拡張します。
  2. 将来的なサイレントバグを防ぐため、a2typeが理解できない命令に遭遇した場合にコンパイラをクラッシュさせる(fatalエラーを発生させる)ように変更します。

また、このコミットには、liblinkのマージによりProg.pcvlong型になったことに起因する、アセンブリリストのpred(先行命令)およびsucc(後続命令)情報をダンプする際のprint文のバグ修正も含まれています。%.4udフォーマット指定子にint型の引数を渡すように修正され、これにより現在のARMビルドの破損が解消されるはずです。

変更の背景

このコミットの背景には、Goコンパイラのレジスタ最適化における正確性の問題と、コンパイラの堅牢性の向上が挙げられます。

  1. レジスタ最適化のバグ: cmd/5gのコピー伝播フェーズにおけるレジスタタイプの誤認識は、コンパイラが生成するコードの正確性に直接影響を与えます。特にCMP $0, R2のような一般的な命令で問題が発生することは、コンパイラの信頼性を損なう重大なバグです。レジスタの誤った認識は、不適切なレジスタ割り当てや、最適化の失敗、さらには誤ったコード生成につながる可能性があります。
  2. コンパイラ設計の一貫性: cmd/5cが既にオペコードに基づいてレジスタ型を決定する堅牢なメカニズム(a2type)を持っていたにもかかわらず、cmd/5gが異なる、かつ脆弱なヒューリスティックを使用していたことは、コンパイラバックエンド間の設計の一貫性の欠如を示唆しています。このコミットは、より良い設計パターンを共有することで、コンパイラ全体の品質を向上させます。
  3. 将来的なバグの防止: a2typeが未知の命令に遭遇した場合にコンパイラをクラッシュさせるように変更することは、非常に重要な改善です。これにより、新しい命令が追加された際に、コンパイラがその命令を誤って処理したり、サイレントに間違ったコードを生成したりするリスクを低減します。明示的なクラッシュは、開発者が問題を早期に発見し、修正することを促します。
  4. ビルドの安定性: Prog.pcの型変更によるビルド破損は、コンパイラの開発プロセスにおける安定性の問題です。liblinkのマージという大きな変更に伴う副作用を修正することで、開発者がGoコンパイラをビルドし、テストする際の障壁を取り除きます。

これらの背景から、このコミットはGoコンパイラの正確性、堅牢性、そして開発の安定性を向上させるための重要な修正であると言えます。

前提知識の解説

このコミットを理解するためには、以下のGoコンパイラの内部構造と最適化に関する基本的な知識が必要です。

  1. Goコンパイラの構造:

    • Goコンパイラは、フロントエンド、ミドルエンド、バックエンドの3つの主要なフェーズに分かれています。
    • フロントエンド: ソースコードの字句解析、構文解析、意味解析を行い、抽象構文木(AST)を生成します。
    • ミドルエンド: ASTを中間表現(IR)に変換し、プラットフォーム非依存の最適化(例: インライン化、デッドコード削除)を行います。
    • バックエンド: IRをターゲットアーキテクチャの機械語に変換し、プラットフォーム依存の最適化(例: レジスタ割り当て、命令スケジューリング)を行います。
    • cmd/5g, cmd/6g, cmd/8gなどは、それぞれARM (5g), x86-64 (6g), x86 (8g) などの異なるアーキテクチャ向けのバックエンドコンパイラを指します。
  2. レジスタ最適化 (Register Optimization):

    • コンパイラ最適化の一種で、プログラムの実行速度を向上させるために、CPUのレジスタを効率的に使用することを目的とします。
    • レジスタはメモリよりもアクセスが高速であるため、頻繁に使用される値をレジスタに保持することでパフォーマンスが向上します。
    • レジスタ割り当て(Register Allocation)は、変数をどのレジスタに割り当てるかを決定するプロセスです。
  3. コピー伝播 (Copy Propagation):

    • データフロー最適化の一種です。
    • x = y のようなコピー命令がある場合、その後のコードでxが使用されている箇所をyに置き換えることで、コピー命令自体を削除したり、レジスタの使用を最適化したりします。
    • 例:
      a = b
      c = a + 1
      
      c = b + 1
      
      に変換する。
    • この最適化は、レジスタの再利用や、不要なデータ移動の削減に貢献します。
  4. 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: 命令のデスティネーションオペランド。
  5. レジスタの種類:

    • D_REG: 汎用レジスタ(整数演算などに使用)。
    • D_FREG: 浮動小数点レジスタ(浮動小数点演算などに使用)。
    • NREG: 無効なレジスタ、またはレジスタが指定されていないことを示す。
    • D_NONE: オペランドが存在しないことを示す。
  6. 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)であり、レジスタではありません。そのため、copyau1p->fromからp->regの型を推測することができず、p->reg==2R2への参照であることを正しく認識できませんでした。結果として、コピー伝播の際にレジスタの置き換え(リネーム)が適切に行われず、最適化が失敗したり、誤ったコードが生成されたりする可能性がありました。

修正後の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->regNREG(レジスタが指定されていない)の場合は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進整数)で渡していました。

%.4udint型の引数を期待するため、vlong型の値をそのまま渡すと、コンパイラが警告を発したり、場合によっては未定義の動作を引き起こしたりする可能性があります。このコミットでは、print(" %.4ud", r1->prog->pc);という行をprint(" %.4ud", (int)r1->prog->pc);と修正し、明示的にvlong型のpcint型にキャストするようにしました。これにより、フォーマット指定子と引数の型が一致し、ARMビルドの破損が解消されました。これは、コンパイラのデバッグ出力の正確性と、ビルドの安定性を確保するための重要な修正です。

これらの変更は、Goコンパイラのバックエンドにおけるレジスタ最適化の正確性を向上させ、将来的なバグの発生を防ぐための堅牢なメカニズムを導入し、さらにビルドの安定性も確保しています。

コアとなるコードの変更箇所

このコミットのコアとなるコードの変更は、主にsrc/cmd/5g/peep.cに集中しています。

src/cmd/5g/peep.c

  1. 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;
    +	}
    +}
    +
    
  2. 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.csrc/cmd/8g/reg.cにも適用されています。

コアとなるコードの解説

src/cmd/5g/peep.c

  1. 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エラーを発生させてクラッシュします。これにより、将来的に新しい命令が追加された際に、その命令のレジスタ型が未定義のまま処理されることによるサイレントバグを防ぎ、開発者に問題を早期に通知します。
  2. copyau1関数の修正:

    • この関数は、コピー伝播のロジックの一部であり、特定のレジスタvが命令pの中央レジスタp->regと一致するかどうか、およびその型が正しいかを比較します。
    • 修正前: 複雑な条件分岐を使用して、p->fromp->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言語のコンパイラに関するドキュメントやソースコード(特にsrc/cmd/5g/, src/cmd/6g/, src/cmd/8g/ディレクトリ内のファイル)。
  • コンパイラ最適化(レジスタ割り当て、コピー伝播など)に関する一般的な情報源。
  • Go言語のliblinkに関する情報。
  • Go言語のProgおよびAdr構造体に関する定義(通常はsrc/cmd/internal/obj/や各アーキテクチャのobjパッケージ内)。
  • Go言語のfatal関数に関する情報(通常はコンパイラのユーティリティ関数として定義されている)。
  • C言語のprintfフォーマット指定子に関する情報。