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

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

コミット

Author: Daniel Morsing daniel.morsing@gmail.com Date: Wed Feb 27 19:47:14 2013 +0100 Commit Hash: 43c04ba1b8c81114d09c0f9f326987070e0ee291

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

https://github.com/golang/go/commit/43c04ba1b8c81114d09c0f9f326987070e0ee291

元コミット内容

cmd/gc: don't emit type instructions for nonexisting locals

If all locals are optimized away, the type instructions would stay in the instruction stream. Call fixautoused to scrub the output.

Fixes #4915.

R=rsc
CC=golang-dev
https://golang.org/cl/7385055

変更の背景

このコミットは、Goコンパイラ(cmd/gc)における特定のバグを修正するために行われました。問題は、関数内のローカル変数(自動変数)がコンパイラの最適化によって完全に除去された場合でも、そのローカル変数に関する型情報(type instructions)が誤って命令ストリーム(生成されるバイナリコード)に残ってしまうというものでした。

これにより、生成されるコードには不要なデバッグ情報やメタデータが含まれることになり、バイナリサイズの増大や、場合によってはデバッグ時の混乱を招く可能性がありました。特に、すべてのローカル変数が最適化によって消滅するような極端なケースでこの問題が顕在化していました。

この問題はGoのIssue #4915として報告されており、このコミットはその修正を目的としています。

前提知識の解説

このコミットの理解を深めるために、以下のGoコンパイラおよび関連する概念についての前提知識が必要です。

  • Goコンパイラ (cmd/gc): Go言語の公式ツールチェインに含まれる主要なコンパイラです。Goのソースコードを機械語に変換する役割を担います。cmd/gcは、Go言語のセマンティクスを理解し、最適化を行い、最終的な実行可能ファイルを生成します。
  • ローカル変数 (Automatic Variables / Locals): 関数内で宣言され、その関数の実行スコープ内でのみ有効な変数です。Goコンパイラ内部では「自動変数(PAUTO)」として扱われます。関数が呼び出されるたびにスタック上に割り当てられ、関数が終了すると解放されます。
  • コンパイラの最適化 (Compiler Optimization): コンパイラが生成する機械語コードの性能(実行速度、メモリ使用量、バイナリサイズなど)を向上させるためのプロセスです。使用されていない変数や到達不能なコードの除去(デッドコードエリミネーション)なども最適化の一環として行われます。このコミットの文脈では、使用されていないローカル変数が最適化によって除去されることが問題の根源でした。
  • 型情報 (Type Instructions): コンパイラが生成するバイナリコードには、プログラムの実行に必要な機械語命令だけでなく、デバッグやランタイムの型チェック、ガベージコレクションなどのために、変数や関数の型に関するメタデータが含まれることがあります。これらは「型命令」や「型記述子」などと呼ばれることがあります。
  • 命令ストリーム (Instruction Stream): CPUが実行する一連の機械語命令のことです。コンパイラはソースコードを解析し、この命令ストリームを生成します。
  • fixautoused 関数: Goコンパイラの内部関数の一つで、主にsrc/cmd/gc/pgen.c(またはその後のバージョンではsrc/cmd/compile/internal/gc/pgen.goなど)に存在します。この関数は、自動変数(ローカル変数)の使用状況を分析し、不要になったり最適化によって除去されたりした自動変数に関連する情報を、生成されるコードから「スクラブ(除去)」する役割を担います。具体的には、デバッグ情報や型情報など、もはや必要のないメタデータをクリーンアップします。
  • Goコンパイラ内部のノード表現 (Node, ONAME, PAUTO, n->used):
    • Goコンパイラは、ソースコードを抽象構文木(AST)として内部的に表現します。このASTの各要素は「ノード(Node)」として扱われます。
    • ONAME: ノードの種類の一つで、名前付きエンティティ(変数、関数など)を表します。
    • PAUTO: ONAMEノードのclass属性の一つで、その名前付きエンティティが自動変数(ローカル変数)であることを示します。
    • n->used: ノード(変数)がコード内で実際に使用されているかどうかを示すフラグです。コンパイラの最適化段階で、このフラグがfalseであれば、その変数は除去の対象となりえます。

技術的詳細

このコミットが修正する問題は、Goコンパイラのコード生成フェーズ、特にローカル変数の割り当てと管理を行う部分にありました。src/cmd/gc/pgen.cファイル内のallocauto関数は、関数の自動変数(ローカル変数)を処理し、それらのスタック上のオフセットを決定し、必要に応じてデバッグ情報や型情報を生成する役割を担っています。

問題のシナリオは以下の通りです。

  1. ある関数に複数のローカル変数が宣言されている。
  2. コンパイラの最適化フェーズで、これらのローカル変数がすべて「使用されていない(!n->used)」と判断され、コードから完全に除去される。
  3. allocauto関数内で、すべてのローカル変数が除去された場合(n->class == PAUTO && n->op == ONAME && !n->usedの条件が真となる場合)、curfn->dcl = nil(現在の関数の宣言リストをクリア)およびstksize = 0(スタックサイズをゼロに設定)が実行されます。これは、ローカル変数が存在しないことを正しく反映する処理です。
  4. しかし、この時点では、以前に生成された可能性のある、除去されたローカル変数に関連する「型命令」が命令ストリーム中に残ってしまう可能性がありました。curfn->dclnilに設定するだけでは、これらの残存する型情報を完全にクリーンアップするには不十分でした。

このコミットは、このギャップを埋めるために、すべてのローカル変数が最適化によって除去された場合に、明示的にfixautoused(ptxt)関数を呼び出すように変更しました。fixautousedは、命令ストリームを走査し、もはや有効ではない自動変数に関連する型命令やデバッグ情報を「スクラブ」する(除去する)役割を担っています。これにより、生成されるバイナリコードから不要な型情報が確実に削除され、問題が解決されます。

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

--- a/src/cmd/gc/pgen.c
+++ b/src/cmd/gc/pgen.c
@@ -209,8 +209,10 @@ allocauto(Prog* ptxt)
 	ll = curfn->dcl;
 	n = ll->n;
 	if (n->class == PAUTO && n->op == ONAME && !n->used) {
+\t\t// No locals used at all
 	\tcurfn->dcl = nil;
 	\tstksize = 0;
+\t\tfixautoused(ptxt);
 	\treturn;
 	}

コアとなるコードの解説

変更はsrc/cmd/gc/pgen.cファイルのallocauto関数内で行われています。

allocauto関数は、Goコンパイラが関数の自動変数(ローカル変数)を処理する際に呼び出される重要な関数です。この関数は、ローカル変数の宣言リスト(curfn->dcl)を走査し、それぞれの変数に対してスタック上の領域を割り当てたり、関連するメタデータを処理したりします。

変更が加えられたのは、以下のif文のブロック内です。

	if (n->class == PAUTO && n->op == ONAME && !n->used) {
		// No locals used at all
		curfn->dcl = nil;
		stksize = 0;
		fixautoused(ptxt); // 追加された行
		return;
	}

このif条件は、特定のシナリオを検出します。

  • n->class == PAUTO: 現在処理しているノードが自動変数(ローカル変数)である。
  • n->op == ONAME: そのノードが名前付きエンティティである。
  • !n->used: そのローカル変数がコード内で全く使用されていない(つまり、最適化によって除去されるべきである)。

この条件が真となるのは、関数内のすべてのローカル変数が最適化によって除去され、もはや存在しないと判断された場合です。

このブロック内で元々行われていた処理は以下の通りです。

  • curfn->dcl = nil;: 現在の関数のローカル変数宣言リストをクリアします。これにより、コンパイラはこれらの変数が存在しないものとして扱います。
  • stksize = 0;: 関数のスタックフレームサイズをゼロに設定します。これは、ローカル変数が存在しないため、スタック領域が不要であることを示します。

このコミットによって、上記の処理に加えて以下の行が追加されました。

  • fixautoused(ptxt);: これがこのコミットの核心です。ptxtは関数のプロローグ命令へのポインタです。fixautoused関数は、このプロローグ命令から始まる命令ストリームを走査し、もはや存在しない自動変数(ローカル変数)に関連する不要な型命令やデバッグ情報を探し出して除去します。これにより、最適化によって除去されたローカル変数の「痕跡」がバイナリコードに残ることを防ぎ、クリーンな出力を保証します。

この修正により、Goコンパイラは、ローカル変数が完全に最適化された場合でも、正確で最小限のバイナリコードを生成できるようになりました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメントおよびソースコード
  • Goコンパイラの内部構造に関する一般的な情報源
  • Go Issue #4915の議論内容
  • Go CL 7385055のコードレビューコメント
  • fixautoused関数の役割に関するGoコンパイラのソースコード分析
  • Goコンパイラの最適化に関する一般的な情報