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

[インデックス 15042] ファイルの概要

このコミットは、Goコンパイラ(cmd/gc)におけるエクスポートデータ生成の2つのバグを修正するものです。具体的には、積極的なインライン化(aggressive inlining)が有効な場合に発生する、サポートされていないメソッド宣言のエクスポートと、recover()関数のエクスポートデータにおける不適切なフォーマットの問題に対処しています。

コミット

commit fc7b75f21622a3c4ddb523f49a24274ffcf41147
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Wed Jan 30 21:10:19 2013 +0100

    cmd/gc: fix export data for aggressive inlining.
    
    Export data was broken after revision 6b602ab487d6
    when -l is specified at least 3 times: it makes the compiler
    write out func (*T).Method() declarations in export data, which
    is not supported.
    
    Also fix the formatting of recover() in export data. It was
    not treated like panic() and was rendered as "<node RECOVER>".
    
    R=golang-dev, lvd, minux.ma, rsc
    CC=golang-dev
    https://golang.org/cl/7067051

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/fc7b75f21622a3c4ddb523f49a24274ffcf41147

元コミット内容

このコミットは、Goコンパイラ(cmd/gc)のエクスポートデータに関する2つの問題を修正します。

  1. 積極的なインライン化によるエクスポートデータの破損:

    • 以前のコミット 6b602ab487d6 以降、コンパイラオプション -l が3回以上指定される(積極的なインライン化が有効になる)と、エクスポートデータが破損する問題が発生していました。
    • この問題は、コンパイラが func (*T).Method() のようなメソッド宣言をエクスポートデータに書き出してしまうことに起因していました。このような内部的なメソッド宣言はエクスポートデータに含めるべきではなく、サポートされていませんでした。
  2. recover() 関数のフォーマット問題:

    • recover() 関数がエクスポートデータ内で正しくフォーマットされていませんでした。
    • panic() 関数とは異なり、recover()<node RECOVER> のように汎用的なノードとして表現されてしまい、その本来のセマンティクスが失われていました。

変更の背景

Goコンパイラは、コンパイルされたパッケージの情報を他のパッケージが利用できるように「エクスポートデータ」として出力します。このエクスポートデータには、関数、型、変数などのエクスポートされたシンボルに関する情報が含まれます。

このコミットの背景には、以下の2つの主要な問題がありました。

  1. コンパイラの最適化(インライン化)とエクスポートデータの整合性: Goコンパイラは、パフォーマンス向上のために様々な最適化を行います。その一つが「インライン化」です。インライン化は、小さな関数呼び出しを呼び出し元のコードに直接展開することで、関数呼び出しのオーバーヘッドを削減します。Goコンパイラには、-l フラグを複数回指定することで、より積極的なインライン化を指示するオプションがありました。しかし、この積極的なインライン化が、コンパイラが内部的に生成するメソッド関連のノードを、誤ってエクスポートデータに含めてしまうという副作用を引き起こしていました。エクスポートデータは、外部から参照可能なAPIインターフェースを記述するものであり、コンパイラ内部の最適化によって生成される一時的な構造や、外部に公開されない内部的なメソッド宣言が含まれるべきではありませんでした。この不整合が、他のパッケージがこのエクスポートデータを利用しようとした際に問題を引き起こす可能性がありました。

  2. recover() 関数の特殊な性質とコンパイラの扱い: Go言語の recover() 関数は、panic() からの回復を試みるための特殊な組み込み関数です。コンパイラは、このような特殊な組み込み関数を適切に処理し、そのセマンティクスをエクスポートデータにも正確に反映させる必要があります。しかし、このコミット以前は、recover()panic() と同様に特別扱いされておらず、エクスポートデータ上では単なる汎用的なノードとして扱われていました。これにより、エクスポートデータを利用するツールや他のコンポーネントが recover() の存在や意味を正確に解釈できない可能性がありました。

これらの問題は、Goコンパイラの安定性と、生成されるバイナリおよびエクスポートデータの正確性に影響を与えるものであり、修正が求められていました。

前提知識の解説

このコミットを理解するためには、以下のGo言語およびコンパイラに関する基本的な知識が必要です。

  • Goコンパイラ (cmd/gc): Go言語の公式コンパイラであり、Goソースコードを機械語に変換します。gc は "Go Compiler" の略です。
  • エクスポートデータ (Export Data): Goコンパイラがパッケージをコンパイルする際に生成するメタデータの一種です。このデータには、そのパッケージが外部に公開している(エクスポートしている)型、関数、変数などの情報が含まれます。他のパッケージがそのパッケージをインポートする際に、このエクスポートデータを読み込むことで、依存関係を解決し、型チェックやリンケージを行うことができます。エクスポートデータは通常、コンパイル済みパッケージの .a ファイル(アーカイブファイル)内に埋め込まれています。
  • インライン化 (Inlining): コンパイラの最適化手法の一つ。関数呼び出しのオーバーヘッド(スタックフレームの作成、引数の渡し、戻り値の処理など)を削減するために、呼び出される関数の本体を呼び出し元のコードに直接埋め込む(インライン展開する)ことです。Goコンパイラでは、-l フラグを使ってインライン化の積極性を調整できます。-l を複数回指定すると、より積極的にインライン化が行われます。
  • panic()recover(): Go言語におけるエラーハンドリングメカニズムの一部です。
    • panic(): プログラムの実行を即座に停止させ、現在のゴルーチンをパニック状態にします。パニックは、通常、回復不可能なエラーやプログラマの論理的誤りを示すために使用されます。
    • recover(): defer 関数内で呼び出された場合にのみ有効で、パニック状態から回復し、パニックの原因となった値を返すことができます。これにより、プログラムがクラッシュするのを防ぎ、エラーを適切に処理する機会を提供します。
  • AST (Abstract Syntax Tree): 抽象構文木。ソースコードの構文構造を木構造で表現したものです。コンパイラはソースコードをパースしてASTを構築し、その後の最適化やコード生成のフェーズでこのASTを操作します。
  • ノード (Node): ASTを構成する個々の要素。関数、変数、式、文など、コードの各部分がノードとして表現されます。
  • Node->op: Goコンパイラの内部表現において、ノードの種類(演算子、キーワード、特定の構文要素など)を示すフィールド。例えば、ORECOVERrecover() 関数呼び出しを表す内部的なオペレーションコードです。
  • Node->class: ノードが表すシンボルの種類(関数、外部シンボルなど)を示すフィールド。PFUNC は関数、PEXTERN は外部シンボルを表します。
  • Node->type: ノードが表す式の型情報。
  • Node->sym: ノードが表すシンボル(変数名、関数名など)へのポインタ。
  • thistuple: Goコンパイラの内部で、メソッドのレシーバ(func (t *T) Method()t *T の部分)に関する情報を保持するフィールド。thistuple > 0 は、そのノードがレシーバを持つメソッド呼び出しに関連していることを示唆します。
  • Gerrit: Googleが開発したオープンソースのコードレビューシステム。Goプロジェクトでは、GitHubにコミットされる前にGerritでコードレビューが行われます。コミットメッセージにある https://golang.org/cl/7067051 はGerritのチェンジリスト(Change-ID)へのリンクです。

技術的詳細

このコミットは、Goコンパイラのバックエンド部分、特にエクスポートデータ生成とASTの処理に関連する部分に焦点を当てています。

1. 積極的なインライン化によるエクスポートデータの破損の修正

この問題は、src/cmd/gc/export.c ファイルの reexportdep 関数内で修正されています。reexportdep 関数は、エクスポートデータに含めるべき依存関係を再帰的に探索し、exportlist に追加する役割を担っています。

問題の根源は、積極的なインライン化が有効な場合、コンパイラが内部的に生成するメソッド呼び出しに関連するASTノードが、誤ってエクスポート対象として処理されてしまうことにありました。特に、func (*T).Method() のようなメソッド宣言(またはその内部表現)がエクスポートデータに書き出されると、これは外部から利用されるべき情報ではないため、エクスポートデータのフォーマット違反や意味的な誤りにつながりました。

修正は、reexportdep 関数内の PFUNC (関数) ノードの処理ロジックに追加されています。

		case PFUNC:
			// methods will be printed along with their type
			// nodes for T.Method expressions
			if(n->left && n->left->op == OTYPE)
				break;
			// nodes for method calls.
			if(!n->type || n->type->thistuple > 0)
				break; // <-- この行が追加された主要な修正
			// fallthrough
		case PEXTERN:

追加された if(!n->type || n->type->thistuple > 0) という条件は、以下のいずれかのケースに該当する場合に、そのノードをエクスポートリストに追加しないようにします。

  • !n->type: ノードに型情報がない場合。これは通常、不完全なノードや内部的な一時ノードを示唆します。
  • n->type->thistuple > 0: ノードがレシーバを持つメソッド呼び出しに関連している場合。thistuple はメソッドのレシーバ引数の数を表す内部的なフィールドであり、これが0より大きいということは、そのノードが func (t *T) Method() のようなメソッドの内部表現、またはその呼び出しに関連していることを意味します。

この修正により、コンパイラは、外部に公開されるべきではない内部的なメソッド関連のノードをエクスポートデータから除外するようになり、エクスポートデータの整合性が保たれます。

2. recover() 関数のフォーマット問題の修正

この問題は、src/cmd/gc/fmt.c ファイルで修正されています。fmt.c は、Goコンパイラの内部表現(ASTノードなど)を人間が読める形式や、エクスポートデータに適した形式にフォーマットする役割を担っています。

問題は、recover() 関数が panic() と同様に特別な組み込み関数として認識されず、エクスポートデータ内で <node RECOVER> のような汎用的なプレースホルダーとして出力されてしまうことでした。これは、コンパイラの内部で recover() に対応するオペレーションコード (ORECOVER) が、その文字列表現にマッピングされていなかったためです。

修正は2箇所で行われています。

  1. goopnames 配列への追加: goopnames は、Goコンパイラの内部オペレーションコードとその文字列表現をマッピングする配列です。ここに ORECOVER のエントリが追加されました。

    // src/cmd/gc/fmt.c
    goopnames[] =
    	// ...
    	[ORECV]		= "<-",
    	[ORECOVER]	= "recover", // <-- この行が追加
    	[ORETURN]	= "return",
    	// ...
    

    これにより、ORECOVER という内部コードが recover という文字列として認識されるようになります。

  2. exprfmt 関数での処理の追加: exprfmt 関数は、式ノードをフォーマットする際に使用されます。recover() は式として扱われるため、この関数内で適切に処理される必要があります。修正では、ORECOVEROPANIC などと同様に、引数を持つ関数呼び出しとしてフォーマットされるように、switch ステートメントに case ORECOVER: が追加されました。

    // src/cmd/gc/fmt.c
    exprfmt(Fmt *f, Node *n, int prec)
    {
    	// ...
    	case OMAKE:
    	case ONEW:
    	case OPANIC:
    	case ORECOVER: // <-- この行が追加
    	case OPRINT:
    	case OPRINTN:
    		if(n->left)
    			// ...
    

    この変更により、recover() がエクスポートデータ内で recover() と正しく表示されるようになり、そのセマンティクスが正確に伝わるようになりました。

これらの修正は、Goコンパイラが生成するエクスポートデータの正確性と互換性を向上させ、Go言語の機能がコンパイラの内部で一貫して扱われることを保証します。

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

src/cmd/gc/export.c

--- a/src/cmd/gc/export.c
+++ b/src/cmd/gc/export.c
@@ -106,12 +106,19 @@ reexportdep(Node *n)
 		switch(n->class&~PHEAP) {
 		case PFUNC:
 			// methods will be printed along with their type
+			// nodes for T.Method expressions
 			if(n->left && n->left->op == OTYPE)
 				break;
+			// nodes for method calls.
+			if(!n->type || n->type->thistuple > 0)
+				break;
 			// fallthrough
 		case PEXTERN:
 			if(n->sym && !exportedsym(n->sym)) {
+				if(debug['E'])
+					print("reexport name %S\n", n->sym);
 				exportlist = list(exportlist, n);
+			}
 		}
 		break;
 
@@ -122,6 +129,8 @@ reexportdep(Node *n)
 			if(isptr[t->etype])
 				t = t->type;
 			if(t && t->sym && t->sym->def && !exportedsym(t->sym)) {
+				if(debug['E'])
+					print("reexport type %S from declaration\n", t->sym);
 				exportlist = list(exportlist, t->sym->def);
 			}
 		}

src/cmd/gc/fmt.c

--- a/src/cmd/gc/fmt.c
+++ b/src/cmd/gc/fmt.c
@@ -228,6 +228,7 @@ goopnames[] =
 	[ORANGE]	= "range",
 	[OREAL]		= "real",
 	[ORECV]		= "<-",
+	[ORECOVER]	= "recover",
 	[ORETURN]	= "return",
 	[ORSH]		= ">>",
 	[OSELECT]	= "select",
@@ -1290,6 +1291,7 @@ exprfmt(Fmt *f, Node *n, int prec)
 	case OMAKE:
 	case ONEW:
 	case OPANIC:
+	case ORECOVER:
 	case OPRINT:
 	case OPRINTN:
 		if(n->left)

コアとなるコードの解説

src/cmd/gc/export.c の変更点

reexportdep 関数は、コンパイラがエクスポートデータに含めるべきシンボルを決定する際に使用されます。

  • case PFUNC: ブロック内の変更:
    • 既存の if(n->left && n->left->op == OTYPE) break; は、型に関連付けられたメソッド(例: T.Method のような式)を処理するためのものでした。
    • 新たに追加された if(!n->type || n->type->thistuple > 0) break; が、積極的なインライン化によって誤ってエクスポートリストに追加されてしまうメソッド関連のノードを除外する主要な修正です。
      • !n->type: ノードに有効な型情報がない場合。これは、コンパイラが内部的に生成した一時的なノードである可能性が高いです。
      • n->type->thistuple > 0: ノードがレシーバを持つメソッド呼び出し(例: obj.Method())に関連している場合。thistuple は、メソッドのレシーバ引数の数を表すコンパイラ内部のフィールドです。これが0より大きい場合、それはレシーバを持つメソッドであることを意味し、このようなノードは通常、エクスポートデータに直接含めるべきではありません。
    • この break により、これらの条件に合致するノードは exportlist に追加されなくなり、エクスポートデータの破損が防がれます。
  • デバッグ出力の追加:
    • if(debug['E']) print("reexport name %S\n", n->sym);if(debug['E']) print("reexport type %S from declaration\n", t->sym); は、コンパイラのデバッグフラグ -E が有効な場合に、どのシンボルがエクスポートリストに追加されようとしているかを出力するためのものです。これは、デバッグや問題の診断に役立ちます。

src/cmd/gc/fmt.c の変更点

fmt.c は、コンパイラの内部表現を文字列に変換するフォーマット処理を担当します。

  • goopnames 配列への ORECOVER の追加:
    • goopnames は、Go言語の組み込み演算子やキーワードに対応する内部オペレーションコード(O で始まる定数)とその文字列表現をマッピングするテーブルです。
    • [ORECOVER] = "recover", の追加により、コンパイラが ORECOVER という内部コードを処理する際に、その文字列表現として "recover" を使用できるようになりました。これにより、エクスポートデータやデバッグ出力で recover() が正しく表示されるようになります。
  • exprfmt 関数への case ORECOVER: の追加:
    • exprfmt 関数は、Goの式ノードをフォーマットするロジックを含んでいます。
    • case ORECOVER:OMAKE, ONEW, OPANIC, OPRINT, OPRINTN などと同じ switch ブロックに追加されました。これは、recover() がこれらの関数と同様に、引数を持つ(または引数を取らない)関数呼び出しとして扱われ、適切にフォーマットされるべきであることを示しています。この変更により、recover() がエクスポートデータ内で <node RECOVER> のような汎用的な表現ではなく、recover() という正しい形式で出力されるようになります。

これらの変更は、Goコンパイラの内部的な整合性を高め、特にエクスポートデータがGo言語のセマンティクスを正確に反映するようにするための重要な修正です。

関連リンク

参考にした情報源リンク

  • Go言語の panicrecover について: https://go.dev/blog/defer-panic-and-recover
  • Goコンパイラの内部構造に関する一般的な情報(Goのソースコードや関連するブログ記事、カンファレンストークなど)
  • Goコンパイラのインライン化に関する情報(Goのソースコードや関連するブログ記事など)
  • GoのGerritコードレビューシステムに関する情報: https://go.dev/doc/contribute#gerrit
  • GoコンパイラのASTノードの種類や内部表現に関する情報(Goのソースコード内の定義など)