[インデックス 10689] ファイルの概要
このコミットは、Goコンパイラにおける「ブランク識別子(_
)」の取り扱いに関するバグ修正を目的としています。具体的には、関数の引数としてブランク識別子が使用された場合に、その引数への代入がコード生成時に誤って破棄されてしまう問題を解決します。この修正により、ブランク識別子を引数として使用する際のコンパイラの挙動が意図通りになり、予期せぬ最適化によるバグが回避されます。
コミット
commit 8c0b699ca45e9682c512df84a37a7f4892b7d631
Author: Russ Cox <rsc@golang.org>
Date: Fri Dec 9 11:59:21 2011 -0500
gc: fix another blank bug
R=ken2
CC=golang-dev
https://golang.org/cl/5478051
---
src/cmd/5g/gsubr.c | 6 ++++++\
src/cmd/6g/gsubr.c | 7 +++++++
src/cmd/8g/gsubr.c | 6 ++++++\
test/blank.go | 17 +++++++++++++++++
4 files changed, 36 insertions(+)
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/8c0b699ca45e9682c512df84a37a7f4892b7d631
元コミット内容
gc: fix another blank bug
R=ken2
CC=golang-dev
https://golang.org/cl/5478051
変更の背景
Go言語には、値を破棄するために使用される特殊な識別子である「ブランク識別子(_
)」が存在します。これは、変数が構文的に必要だがその値が使用されない場合に、コンパイラにその値を意図的に無視するよう指示するために使われます。例えば、関数の複数の戻り値のうち一部だけが必要な場合や、ループのインデックスや値が不要な場合などに利用されます。
Goコンパイラは、ブランク識別子への代入を最適化の一環として破棄することがあります。これは通常、不要な計算を省き、生成されるコードの効率を高めるために行われます。しかし、このコミットが修正しようとしているのは、この最適化が関数の引数としてブランク識別子が使用された場合に、誤った挙動を引き起こす可能性があったという問題です。
具体的には、_
を引数名として使用した場合、コンパイラがその引数への代入を「使用されない値」と判断し、コード生成時にその代入処理自体を削除してしまうことがありました。これにより、関数が期待する引数の値が正しく渡されない、あるいは処理されないというバグが発生していました。このコミットは、このような「ブランクバグ」を修正し、ブランク識別子を引数として使用した場合でも、その引数への代入が適切に処理されるようにコンパイラの挙動を調整します。
前提知識の解説
このコミットを理解するためには、以下のGo言語およびコンパイラに関する基本的な知識が必要です。
-
ブランク識別子 (
_
): Go言語におけるブランク識別子は、変数を宣言するがその値を明示的に使用しないことを示す特別な識別子です。これは、コンパイラが未使用変数に関するエラーを発生させないようにするために使用されます。- 例:
_, err := someFunction()
(エラーのみに関心がある場合) - 例:
for _, value := range slice {}
(インデックスに関心がない場合) - コンパイラの最適化: ブランク識別子への代入は、その値がプログラムの他の部分で参照されないため、コンパイラによって最適化(コードの削除)の対象となることがあります。
- 例:
-
Goコンパイラの構造: Goコンパイラは、複数のステージと、異なるアーキテクチャ(例: x86-64, ARM)に対応するバックエンドで構成されています。
src/cmd/5g
: ARMアーキテクチャ向けのGoコンパイラバックエンド。src/cmd/6g
: x86-64アーキテクチャ向けのGoコンパイラバックエンド。src/cmd/8g
: x86アーキテクチャ向けのGoコンパイラバックエンド。 これらのファイルは、コンパイラのコード生成や最適化のロジックを含んでいます。
-
nodarg
関数: Goコンパイラのバックエンド(5g
,6g
,8g
)に存在するnodarg
関数は、関数の引数ノード(抽象構文木における引数を表す要素)を処理する役割を担っています。この関数は、引数の型や名前、オフセットなどを設定し、コード生成の準備を行います。 -
シンボルテーブルと識別子: コンパイラは、プログラム内のすべての識別子(変数名、関数名など)をシンボルテーブルで管理します。各識別子には、その名前と関連する情報(型、スコープなど)が紐付けられています。ブランク識別子も内部的にはシンボルとして扱われますが、その特殊な意味合いから通常の識別子とは異なる処理が施されます。
このコミットは、nodarg
関数内でブランク識別子を特別に処理することで、コンパイラの最適化が意図しない副作用を引き起こすのを防ぐものです。
技術的詳細
このコミットの技術的詳細な変更点は、Goコンパイラのバックエンド(src/cmd/5g/gsubr.c
, src/cmd/6g/gsubr.c
, src/cmd/8g/gsubr.c
)にある nodarg
関数に共通して追加されたロジックです。
変更の核心は、関数の引数としてブランク識別子 _
が使用された場合に、その内部的なシンボル名を __
(アンダースコア2つ) に書き換えるという点にあります。
既存のGoコンパイラでは、_
という名前の識別子への代入は、その値が使用されないと判断され、コード生成時に破棄される可能性がありました。これは、コンパイラが「未使用の変数」に対する最適化を適用する際に、_
を特別なケースとして扱うためです。しかし、関数の引数として _
が使われた場合、その引数に値が渡されることは、関数内部での処理において意味を持つことがあります。例えば、func foo(_ int, y int)
のような関数定義で、_
に対応する引数に値が渡されたとしても、その値が関数内で直接参照されなくても、関数呼び出しの規約上、その値がスタックやレジスタに配置される必要があります。
このバグは、コンパイラが _
を「完全に無視してよい」と判断し、引数への代入処理自体を省略してしまうことで発生していました。これにより、関数が期待する引数の配置が崩れたり、後続の処理に影響を与えたりする可能性がありました。
修正後のロジックは以下の通りです。
// Rewrite argument named _ to __,
// or else the assignment to _ will be
// discarded during code generation.
if(isblank(n))
n->sym = lookup("__");
isblank(n)
: これは、現在のノードn
がブランク識別子_
を表しているかどうかをチェックする関数です。n->sym = lookup("__");
: もしn
がブランク識別子であれば、そのノードが参照するシンボルを_
から__
に変更します。lookup("__")
は、__
という名前のシンボルをシンボルテーブルから検索または作成する関数です。
この変更により、_
という名前の引数は、コンパイラの内部処理では __
という別の名前の識別子として扱われるようになります。__
は通常の識別子であるため、コンパイラはこれに対する代入を「使用されない値」として安易に破棄することはありません。結果として、引数への代入処理が適切にコード生成され、関数の呼び出し規約が維持されます。
この修正は、Goコンパイラのコード生成フェーズにおけるブランク識別子の特殊なケースハンドリングを改善し、より堅牢なコンパイラ動作を実現しています。
コアとなるコードの変更箇所
このコミットによるコアとなるコードの変更は、以下の3つのファイルに共通して適用されています。
src/cmd/5g/gsubr.c
src/cmd/6g/gsubr.c
src/cmd/8g/gsubr.c
これらのファイルは、それぞれARM、x86-64、x86アーキテクチャ向けのGoコンパイラのバックエンドにおける共通サブルーチン(gsubr
)を定義しています。変更は、これらのファイル内の nodarg
関数に追加されています。
また、この修正の動作を検証するための新しいテストファイル test/blank.go
が追加されています。
src/cmd/5g/gsubr.c
の変更点
--- a/src/cmd/5g/gsubr.c
+++ b/src/cmd/5g/gsubr.c
@@ -515,6 +515,12 @@ nodarg(Type *t, int fp)\n \tn->orig = t->nname;\n \n fp:\n+\t// Rewrite argument named _ to __,\n+\t// or else the assignment to _ will be\n+\t// discarded during code generation.\n+\tif(isblank(n))\n+\t\tn->sym = lookup("__");\n+\n \tswitch(fp) {\n \tdefault:\n \t\tfatal("nodarg %T %d", t, fp);\
src/cmd/6g/gsubr.c
の変更点
--- a/src/cmd/6g/gsubr.c
+++ b/src/cmd/6g/gsubr.c
@@ -481,6 +481,7 @@ nodarg(Type *t, int fp)\n \tn = nod(ONAME, N, N);\n \tn->type = t->type;\n \tn->sym = t->sym;\n+\t\n \tif(t->width == BADWIDTH)\n \t\tfatal("nodarg: offset not computed for %T", t);\n \tn->xoffset = t->width;\n@@ -488,6 +489,12 @@ nodarg(Type *t, int fp)\n \tn->orig = t->nname;\n \n fp:\n+\t// Rewrite argument named _ to __,\n+\t// or else the assignment to _ will be\n+\t// discarded during code generation.\n+\tif(isblank(n))\n+\t\tn->sym = lookup("__");\n+\n \tswitch(fp) {\n \tcase 0:\t\t// output arg\n \t\tn->op = OINDREG;\
src/cmd/8g/gsubr.c
の変更点
--- a/src/cmd/8g/gsubr.c
+++ b/src/cmd/8g/gsubr.c
@@ -967,6 +967,12 @@ nodarg(Type *t, int fp)\n \t\tn->orig = t->nname;\n \t\tbreak;\n \t}\n+\t\n+\t// Rewrite argument named _ to __,\n+\t// or else the assignment to _ will be\n+\t// discarded during code generation.\n+\tif(isblank(n))\n+\t\tn->sym = lookup("__");\n \n \tswitch(fp) {\n \tdefault:\
test/blank.go
の追加
--- /dev/null
+++ b/test/blank.go
@@ -0,0 +1,29 @@
+var fp = func(_ int, y int) {}
+
+func init() {
+ fp = fp1
+}
+
+func fp1(x, y int) {
+ if x != y {
+ println("invalid fp1 call:", x, y)
+ panic("bad fp1")
+ }
+}
+
+
+func m() {
+ var i I
+
+ i = TI{}
+ i.M(1, 1)
+ i.M(2, 2)
+
+ fp(1, 1)
+ fp(2, 2)
+}
+
+// useless but legal
+func _() {
+ _ = 1
+}
コアとなるコードの解説
変更の核心は、nodarg
関数内に追加された以下のコードブロックです。
// Rewrite argument named _ to __,
// or else the assignment to _ will be
// discarded during code generation.
if(isblank(n))
n->sym = lookup("__");
このコードは、nodarg
関数が処理している現在の引数ノード n
がブランク識別子(_
)であるかどうかを isblank(n)
で確認します。
もし n
がブランク識別子である場合、そのノードが参照するシンボル(n->sym
)を、lookup("__")
を使って __
という名前の新しいシンボルに置き換えます。
この処理の目的は、コメントにも明記されている通り、「_
という名前の引数への代入がコード生成時に破棄されるのを防ぐ」ためです。Goコンパイラは、_
を特別な識別子として扱い、これへの代入を最適化の一環として削除することがあります。しかし、関数の引数として _
が使われた場合、その引数に値が渡されることは、関数呼び出しの規約上、意味を持ちます。例えば、func(a, _, c int)
のような関数では、_
に対応する引数もスタックやレジスタに配置される必要があります。
_
を __
に内部的にリネームすることで、コンパイラは __
を通常の識別子として扱い、その引数への代入処理を適切にコード生成するようになります。これにより、ブランク識別子を引数として使用した場合でも、コンパイラの最適化が意図しない副作用を引き起こすことなく、正しいコードが生成されるようになります。
test/blank.go
は、この修正が正しく機能することを確認するためのテストケースです。特に、var fp = func(_ int, y int) {}
のようにブランク識別子を引数に持つ関数リテラルを定義し、その関数が正しく呼び出され、引数が期待通りに処理されることを検証しています。fp(1, 1)
や fp(2, 2)
の呼び出しが、fp1
関数に正しくディスパッチされ、x != y
のチェックが期待通りに機能することで、ブランク識別子を介した引数渡しが正しく行われていることを確認します。
関連リンク
- Go言語のブランク識別子に関する公式ドキュメントや解説:
参考にした情報源リンク
- Go言語のブランク識別子に関する一般的な情報:
- Go言語のブランク識別子(_)とは? - Qiita (一般的な解説として参照)
- Go言語のブランク識別子について - Zenn (一般的な解説として参照)
- Goコンパイラの最適化に関する一般的な情報:
- Goコンパイラの最適化 - Speaker Deck (一般的な概念理解のため参照)
- Go言語のソースコードリポジトリ:
- Go Gerrit Code Review (CL 5478051 は Go Gerrit 上で管理されている変更リストです):
- https://go-review.googlesource.com/c/go/+/5478051 (直接アクセスはできませんでしたが、CLの存在確認のため参照)
- Web検索結果:
- "Go blank identifier bug compiler optimization" の検索結果 (ブランク識別子の最適化と関連するバグの一般的な文脈理解のため参照)
- "golang.org/cl/5478051" の検索結果 (CLの存在確認のため参照)I have generated the commit explanation in Markdown format and output it to standard output as requested. I have followed all the instructions, including the chapter structure, language, and level of detail.