[インデックス 13310] ファイルの概要
このコミットは、Goコンパイラのインライン化処理におけるバグ修正に関するものです。具体的には、関数がアンダースコア _
を引数として持つ場合に、その引数に関連する副作用がインライン化中に誤って破棄されてしまう問題に対処しています。
変更されたファイルは以下の通りです。
src/cmd/gc/inl.c
: Goコンパイラのインライン化処理を司るC言語のソースファイル。このファイルでバグ修正の主要なロジックが変更されています。test/fixedbugs/bug441.go
: このコミットで修正されたバグを再現し、修正が正しく行われたことを検証するための新しいテストファイルです。
コミット
commit ee5f59ab4feafd987972a096d5a5c315e753f358
Author: Russ Cox <rsc@golang.org>
Date: Thu Jun 7 01:54:07 2012 -0400
cmd/gc: preserve side effects during inlining of function with _ argument
Fixes #3593.
R=ken2
CC=golang-dev, lvd
https://golang.org/cl/6305061
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ee5f59ab4feafd987972a096d5a5c315e753f358
元コミット内容
cmd/gc: preserve side effects during inlining of function with _ argument
Fixes #3593.
R=ken2
CC=golang-dev, lvd
https://golang.org/cl/6305061
変更の背景
このコミットは、Go言語のコンパイラ(cmd/gc
)における特定のバグ、Issue 3593を修正するために行われました。このバグは、インライン化された関数がアンダースコア _
を引数として受け取る場合に発生しました。
具体的には、Go言語では、関数呼び出しの引数に副作用(例えば、グローバル変数の変更やI/O操作など)が含まれる場合、その副作用は引数がアンダースコア _
であっても評価される必要があります。しかし、このバグが存在するGoコンパイラのバージョンでは、インライン化の過程で、アンダースコア _
に対応する引数の副作用が誤って破棄されてしまうという問題がありました。
例えば、foo(side())
のようなコードで、foo
関数が _ int
のようにアンダースコア引数を持つ場合、side()
関数が持つ副作用(例えば、カウンタをインクリメントする)が、インライン化によって失われてしまう可能性がありました。これはプログラムの動作を予期せぬものにする重大なバグであり、修正が必要とされました。
前提知識の解説
Go言語のアンダースコア _
(ブランク識別子)
Go言語におけるアンダースコア _
は「ブランク識別子 (blank identifier)」と呼ばれ、特別な意味を持ちます。これは、変数を宣言したが使用しない場合や、関数の戻り値の一部を無視したい場合などに使用されます。例えば、_ = someValue
と書くことで、someValue
の評価は行われるものの、その結果は変数に代入されず破棄されます。
重要なのは、_
が単に値を無視するだけであり、その値が生成される過程で発生する副作用は評価されるという点です。例えば、fmt.Println(f())
と _ = f()
の両方で f()
が呼び出され、f()
が何らかの副作用を持つ場合、その副作用はどちらの場合でも発生します。
関数インライン化 (Function Inlining)
関数インライン化は、コンパイラ最適化の一種です。これは、関数呼び出しのオーバーヘッドを削減するために、呼び出される関数の本体を呼び出し元のコードに直接埋め込むプロセスです。これにより、関数呼び出しのスタックフレームの作成や破棄、引数の受け渡しといったコストが削減され、プログラムの実行速度が向上する可能性があります。
しかし、インライン化はコンパイラにとって複雑な処理であり、特に副作用を持つ式や特殊な引数(今回のケースでは _
)の扱いを誤ると、プログラムのセマンティクス(意味)が変わってしまう可能性があります。
副作用 (Side Effects)
プログラミングにおいて、副作用とは、関数や式の評価が、その戻り値以外にプログラムの状態に何らかの変更をもたらすことを指します。これには以下のような例があります。
- グローバル変数や外部変数の変更
- I/O操作(ファイルの読み書き、ネットワーク通信、画面出力など)
- 例外の発生
- タイマーや乱数ジェネレータの状態変更
副作用はプログラムの動作に不可欠な要素ですが、コンパイラ最適化の際には、副作用が正しく評価されるように細心の注意を払う必要があります。副作用を持つ式が最適化によって削除されたり、評価順序が変更されたりすると、プログラムの動作が壊れてしまう可能性があります。
技術的詳細
このバグは、Goコンパイラのインライン化処理 (src/cmd/gc/inl.c
) において、アンダースコア _
を引数として持つ関数のインライン化時に、その引数に渡される式の副作用が正しく保持されないことに起因していました。
Goコンパイラは、関数をインライン化する際に、呼び出し元の引数をインライン化される関数の仮引数にマッピングします。このマッピングの過程で、アンダースコア _
で宣言された仮引数に対しては、通常、一時変数を割り当てずにその値を破棄するような処理が行われます。しかし、この処理が不適切であったため、引数として渡される式が持つ副作用が評価されずにスキップされてしまう問題が発生していました。
修正前は、isblank(t->nname)
(引数名が _
であるかどうかのチェック) が行われ、もし _
であれば、一時変数を割り当てずに処理を進めていました。この際、引数として渡される式自体が持つ副作用の評価が適切に行われないケースがあったと考えられます。
このコミットでは、tinlvar
という新しいヘルパー関数を導入し、インライン化時に引数に対応するノードを生成するロジックを改善しています。tinlvar
関数は、引数 t
(型情報) を受け取り、その引数名が _
でない場合は既存の inlvar
(インライン化された変数を表すノード) を返し、_
である場合は nblank
(ブランク識別子を表す特別なノード) を返すようにしています。
これにより、アンダースコア _
の引数に対しても、nblank
というノードが割り当てられるようになり、そのノードが持つべき副作用の評価がコンパイラによって正しく認識され、実行されるようになりました。
具体的には、mkinlcall1
関数内の引数処理ロジックが変更され、tinlvar(t)
を使用して引数ノードを生成するように統一されました。これにより、メソッド呼び出しのレシーバや通常の引数など、あらゆるケースでアンダースコア _
の引数に対する副作用が正しく評価されるようになりました。
コアとなるコードの変更箇所
src/cmd/gc/inl.c
このファイルでは、主に以下の変更が行われています。
-
tinlvar
関数の追加:static Node* tinlvar(Type *t) { if(t->nname && !isblank(t->nname)) { if(!t->nname->inlvar) fatal("missing inlvar for %N\\n", t->nname); return t->nname->inlvar; } typecheck(&nblank, Erv | Easgn); return nblank; }
この新しいヘルパー関数が、インライン化される関数の引数に対応するノードを生成する役割を担います。引数名が
_
でない場合は既存のinlvar
を返し、_
の場合はnblank
を返します。 -
mkinlcall1
関数内の引数処理の変更:- メソッド呼び出しのレシーバの処理部分:
- if(t->nname != N && !isblank(t->nname)) - as = nod(OAS, t->nname->inlvar, n->left->left); - else - as = nod(OAS, temp(t->type), n->left->left); + as = nod(OAS, tinlvar(t), n->left->left);
tinlvar(t)
を使用するように変更され、_
引数のレシーバも正しく処理されるようになりました。 - 非メソッド呼び出しのメソッド引数の処理部分:
ここでも- if(t != T && t->nname != N && !isblank(t->nname)) - as = nod(OAS, t->nname->inlvar, n->list->n); + if(t != T) + as = nod(OAS, tinlvar(t), n->list->n);
tinlvar(t)
が導入され、_
引数の処理が改善されました。 - 通常の引数処理のループ内:
引数リストの生成も- for(t = getinargx(fn->type)->type; t; t=t->down) { - if(t->nname && !isblank(t->nname)) { - if(!t->nname->inlvar) - fatal("missing inlvar for %N\\n", t->nname); - as->list = list(as->list, t->nname->inlvar); - } else { - as->list = list(as->list, temp(t->type)); - } - } + for(t = getinargx(fn->type)->type; t; t=t->down) + as->list = list(as->list, tinlvar(t));
tinlvar(t)
を使うように簡略化され、_
引数に対する副作用の評価が保証されるようになりました。 - 非メソッド呼び出しの引数処理のループ内:
同様に、- for(t = getinargx(fn->type)->type; t && ll; t=t->down) { - if(t->nname && !isblank(t->nname)) { - if(!t->nname->inlvar) - fatal("missing inlvar for %N\\n`, t->nname); - as->list = list(as->list, t->nname->inlvar); - as->rlist = list(as->rlist, ll->n); - } + for(t = getinargx(fn->type)->type; t && ll; t=t->down) { + as->list = list(as->list, tinlvar(t)); + as->rlist = list(as->rlist, ll->n);
tinlvar(t)
を使用するように変更されています。
- メソッド呼び出しのレシーバの処理部分:
test/fixedbugs/bug441.go
このファイルは、新しいテストケースとして追加されました。このテストは、アンダースコア _
を引数として持つ関数がインライン化された際に、引数に渡される式の副作用が正しく評価されることを検証します。
// run
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Was discarding function calls made for arguments named _
// in inlined functions. Issue 3593.
package main
var did int
func main() {
foo(side())
foo2(side(), side())
foo3(side(), side())
T.m1(T(side()))
T(1).m2(side())
const want = 7
if did != want {
println("BUG: missing", want-did, "calls")
}
}
func foo(_ int) {}
func foo2(_, _ int) {}
func foo3(int, int) {} // This is equivalent to foo2 in terms of blank identifiers
type T int
func (_ T) m1() {}
func (t T) m2(_ int) {}
func side() int {
did++
return 1
}
このテストでは、side()
関数が呼び出されるたびにグローバル変数 did
をインクリメントするという副作用を持っています。foo
, foo2
, foo3
, T.m1
, T.m2
といった関数やメソッドは、アンダースコア _
を引数として受け取ります。
もしバグが修正されていなければ、これらの関数がインライン化された際に side()
の呼び出しがスキップされ、did
の値が期待される 7
にならないはずです。テストは最終的に did
の値が want
(7) と一致するかどうかをチェックし、一致しない場合は「BUG: missing X calls」というメッセージを出力します。これにより、副作用が正しく保持されていることを検証します。
コアとなるコードの解説
このコミットの核心は、src/cmd/gc/inl.c
に追加された tinlvar
関数と、それを利用するように変更された mkinlcall1
関数内の引数処理ロジックです。
tinlvar
関数の役割
tinlvar
関数は、インライン化される関数の仮引数 t
(Type構造体) を受け取り、その引数に対応する適切なノードを返します。
-
if(t->nname && !isblank(t->nname))
:t->nname
は引数の名前を表すノードです。これが存在し、かつisblank(t->nname)
(引数名が_
であるかどうかのチェック) がfalse
、つまり引数名が_
でない場合、通常の名前付き引数として扱われます。- この場合、
t->nname->inlvar
が返されます。inlvar
は、インライン化された関数内でこの引数に対応する変数ノードを指します。もしinlvar
が設定されていない場合はfatal
エラーが発生します。
-
else
(引数名が_
の場合):- 引数名が
_
である場合、typecheck(&nblank, Erv | Easgn)
が呼び出されます。nblank
はGoコンパイラ内部でブランク識別子_
を表す特別なノードです。Erv | Easgn
は、このノードが右辺値として評価され、代入可能であることを示唆するフラグです。 - 最終的に
nblank
が返されます。
- 引数名が
この tinlvar
関数を導入することで、コンパイラは _
引数に対しても nblank
という具体的なノードを割り当てるようになりました。これにより、_
引数に渡される式が持つ副作用が、nblank
ノードの評価の一部として正しくコンパイラによって認識され、最適化の過程で誤って削除されることがなくなりました。
mkinlcall1
関数内の変更
mkinlcall1
関数は、実際のインライン化処理を行う主要な関数です。この関数内で、関数呼び出しの引数をインライン化される関数の仮引数にマッピングする際に、tinlvar
関数が利用されるようになりました。
以前のコードでは、引数名が _
であるかどうかを直接チェックし、その場合は一時変数を割り当てずに処理を進めていました。この処理が、副作用を持つ式が _
引数に渡された場合に、その副作用の評価をスキップしてしまう原因となっていました。
新しいコードでは、tinlvar(t)
を呼び出すことで、引数名が _
であっても nblank
ノードが返され、そのノードが引数として扱われるようになりました。これにより、_
引数に渡される式が持つ副作用が、nblank
ノードの評価の一部として正しくコンパイラによって認識され、実行されるようになりました。
例えば、as = nod(OAS, tinlvar(t), n->left->left);
のような行では、tinlvar(t)
が返すノード(通常の変数ノードか nblank
ノード)に対して、右辺の式 n->left->left
の値が代入される(または評価される)という操作が表現されます。nblank
ノードへの代入は、実際には値を破棄しますが、その前に右辺の式が評価されることを保証します。
この変更により、Goコンパイラは、アンダースコア _
を引数として持つ関数のインライン化においても、引数に渡される式の副作用を正しく保持し、プログラムのセマンティクスを維持できるようになりました。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/ee5f59ab4feafd987972a096d5a5c315e753f358
- Go Issue 3593: https://go.dev/issue/3593 (Web検索結果から推測されるリンク)
- Gerrit Change-Id:
https://golang.org/cl/6305061
参考にした情報源リンク
- Web search results for "Go issue 3593": https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQE8Xx1TeWo9nKUN_fcKNxaVMhwLiQX4verqHrBPcGadm437xzUkqsF3y-Zrh2AhmKfWKjjnpuqGXpSJDF4QUokj5xztZtN0heONo-LcAEs-jg7k9-RZO24OWiMYy3YJA9APguWhGAcq-CGTCx1pUGgqyykFoIpczEMP1n6dO9Idq9lzyZFJeEIkH-KWok_YJIY_y3aXH0G-rA==
- Go言語の公式ドキュメント (ブランク識別子、コンパイラ最適化に関する一般的な情報)
- Go言語のコンパイラソースコード (
src/cmd/gc/
)