[インデックス 15104] ファイルの概要
このコミットは、Goコンパイラ(cmd/gc
)における最適化に関するもので、特にreturn
ステートメントにおける自己代入(x = x
のような操作)を省略することで、より効率的で競合状態(race condition)が発生しにくいコードを生成することを目的としています。
コミット
commit 2eafbb8878ffc5a032fdc9361ed75731da5bb698
Author: Russ Cox <rsc@golang.org>
Date: Sun Feb 3 01:18:28 2013 -0500
cmd/gc: elide self-assignment during return
More efficient, less racy code.
Fixes #4014.
R=ken2, ken
CC=golang-dev
https://golang.org/cl/7275047
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2eafbb8878ffc5a032fdc9361ed75731da5bb698
元コミット内容
cmd/gc: elide self-assignment during return
More efficient, less racy code.
Fixes #4014.
R=ken2, ken
CC=golang-dev
https://golang.org/cl/7275047
変更の背景
この変更は、Go言語のIssue #4014「go test -race
reports false positive on return
with named result parameters」を修正するために行われました。
Go言語では、関数の戻り値に名前を付ける「名前付き戻り値パラメータ(named result parameters)」という機能があります。例えば、func foo() (x int)
のように定義すると、関数内でx
という変数を宣言せずに使用でき、return
ステートメントで値を指定しない場合、x
の現在の値が戻り値となります。
この名前付き戻り値パラメータを使用する際、コンパイラは内部的に戻り値の代入処理を生成します。例えば、func foo() (a int) { a = 42; return a, 10 }
のようなコードがあった場合、コンパイラはreturn
時にa = a
のような自己代入を生成することがありました。
問題は、Goのレース検出器(race detector)が、このa = a
という自己代入を、同じ変数に対する読み書き操作として検出し、誤ってデータ競合(data race)を報告してしまうケースがあったことです。特に、名前付き戻り値パラメータがクロージャ(goroutine内で実行される関数)によってキャプチャされ、そのクロージャが戻り値の代入と並行して実行される場合に、この誤検出が発生しました。
このコミットは、このような不必要な自己代入をコンパイラが生成しないようにすることで、レース検出器の誤検出をなくし、同時に生成されるコードの効率を向上させることを目的としています。
前提知識の解説
Go言語の名前付き戻り値パラメータ
Go言語の関数は、戻り値に名前を付けることができます。 例:
func calculate(x, y int) (sum int, diff int) {
sum = x + y
diff = x - y
return // sumとdiffの現在の値が返される
}
この機能は、特に複数の戻り値がある場合にコードの可読性を高めるのに役立ちます。関数内でsum
やdiff
といった変数を明示的に宣言する必要がなく、return
ステートメントで値を指定しない場合、それらの変数の現在の値が関数の戻り値となります。
Goのレース検出器(Race Detector)
Goには、並行処理におけるデータ競合を検出するための組み込みツールであるレース検出器があります。これは、go run -race
やgo test -race
のように-race
フラグを付けてプログラムを実行することで有効になります。データ競合は、複数のgoroutineが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みであり、かつそれらのアクセスが同期されていない場合に発生します。データ競合は、プログラムの予測不能な動作やバグの原因となるため、その検出は非常に重要です。
コンパイラの最適化
コンパイラは、ソースコードを機械語に変換する際に、プログラムの実行速度を向上させたり、メモリ使用量を削減したりするために様々な最適化を行います。自己代入の省略(elision)もその一つです。x = x
のような操作は、実際には変数の値を変えないため、コンパイラがこれを認識してコード生成を省略することで、無駄な処理をなくし、より効率的なバイナリを生成できます。
src/cmd/gc/walk.c
src/cmd/gc/walk.c
は、Goコンパイラのバックエンドの一部であり、抽象構文木(AST)を走査(walk)して、コード生成のための準備を行うファイルです。この段階で、コンパイラは様々な最適化や変換を行い、最終的な機械語コードに近づけていきます。特に、代入操作や戻り値の処理に関するロジックが含まれています。
技術的詳細
このコミットの技術的詳細な変更は、src/cmd/gc/walk.c
ファイル内のascompatee
関数にあります。この関数は、代入操作(=
)や戻り値の処理など、複数の値を同時に扱うコンテキストで呼び出されます。
変更前は、ascompatee
関数内で、戻り値の処理(op == ORETURN
)の場合でも、左辺と右辺が同じノード(つまり同じ変数)である場合に、ll->n == lr->n
という条件が考慮されていませんでした。これにより、a = a
のような自己代入が不必要に生成されていました。
変更後は、for
ループ内で各代入ペアを処理する際に、以下の条件が追加されました。
// Do not generate 'x = x' during return. See issue 4014.
if(op == ORETURN && ll->n == lr->n)
continue;
このコードは、現在の操作がORETURN
(戻り値の処理)であり、かつ左辺のノード(ll->n
)と右辺のノード(lr->n
)が同じである(つまり自己代入である)場合に、その代入操作のコード生成をスキップ(continue
)するように指示しています。
これにより、コンパイラは名前付き戻り値パラメータの自己代入を認識し、そのためのコードを生成しなくなります。結果として、不必要なメモリ書き込み操作が削除され、レース検出器が誤ってデータ競合を報告する問題が解決されます。また、生成されるバイナリコードもわずかに効率的になります。
src/pkg/runtime/race/testdata/regression_test.go
に追加されたテストケースTestNoRaceReturn
は、この修正が正しく機能することを確認するためのものです。このテストは、名前付き戻り値パラメータa
を持つ関数noRaceReturn
内で、a
に値を代入し、そのa
をgoroutine内で読み取るというシナリオを再現しています。修正前はここでレースが検出されていましたが、修正後は検出されなくなります。
コアとなるコードの変更箇所
src/cmd/gc/walk.c
--- a/src/cmd/gc/walk.c
+++ b/src/cmd/gc/walk.c
@@ -1326,8 +1326,12 @@ ascompatee(int op, NodeList *nl, NodeList *nr, NodeList **init)
lr->n = safeexpr(lr->n, init);
nn = nil;
- for(ll=nl, lr=nr; ll && lr; ll=ll->next, lr=lr->next)
+ for(ll=nl, lr=nr; ll && lr; ll=ll->next, lr=lr->next) {
+ // Do not generate 'x = x' during return. See issue 4014.
+ if(op == ORETURN && ll->n == lr->n)
+ continue;
nn = list(nn, ascompatee1(op, ll->n, lr->n, init));
+ }
// cannot happen: caller checked that lists had same length
if(ll || lr)
src/pkg/runtime/race/testdata/regression_test.go
--- a/src/pkg/runtime/race/testdata/regression_test.go
+++ b/src/pkg/runtime/race/testdata/regression_test.go
@@ -127,3 +127,21 @@ func divInSlice() {
i := 1
_ = v[(i*4)/3]
}
+
+func TestNoRaceReturn(t *testing.T) {
+ c := make(chan int)
+ noRaceReturn(c)
+ <-c
+}
+
+// Return used to do an implicit a = a, causing a read/write race
+// with the goroutine. Compiler has an optimization to avoid that now.
+// See issue 4014.
+func noRaceReturn(c chan int) (a, b int) {
+ a = 42
+ go func() {
+ _ = a
+ c <- 1
+ }()
+ return a, 10
+}
コアとなるコードの解説
src/cmd/gc/walk.c
の変更
ascompatee
関数は、複数の代入を処理する際に使用されます。この関数は、左辺のノードリストnl
と右辺のノードリストnr
を受け取り、それぞれのペアに対してascompatee1
を呼び出して個別の代入操作を生成します。
追加されたif
文は、このループの内部にあります。
op == ORETURN
: 現在の操作が関数の戻り値の処理であることを示します。ll->n == lr->n
: 左辺のノード(代入先の変数)と右辺のノード(代入元の値)が同じオブジェクトを指していることを示します。これは、a = a
のような自己代入のケースです。
この両方の条件が真である場合、continue
が実行され、現在のループのイテレーションがスキップされます。これにより、ascompatee1
関数が呼び出されなくなり、結果として不必要な自己代入のコードが生成されなくなります。
src/pkg/runtime/race/testdata/regression_test.go
の追加
TestNoRaceReturn
関数は、この修正の回帰テストとして追加されました。
noRaceReturn(c chan int) (a, b int)
: 名前付き戻り値パラメータa
とb
を持つ関数です。a = 42
:a
に初期値を代入します。go func() { _ = a; c <- 1 }()
: 新しいgoroutineを起動し、その中でa
の値を読み取ります。このgoroutineは、a
が戻り値として代入されるのと並行して実行される可能性があります。return a, 10
: ここで、a
の現在の値が戻り値として返されます。修正前は、このreturn
ステートメントが内部的にa = a
のような自己代入を生成し、それがgoroutine内の_ = a
との間でデータ競合を引き起こしていました。
このテストは、修正が適用されたコンパイラではデータ競合が報告されないことを確認します。
関連リンク
- Go Issue #4014: https://github.com/golang/go/issues/4014
- Go CL 7275047: https://golang.org/cl/7275047
参考にした情報源リンク
- Go言語の公式ドキュメント(名前付き戻り値パラメータ、レース検出器に関する情報)
- Goコンパイラのソースコード(
src/cmd/gc/walk.c
の関連部分) - Go Issue #4014の議論スレッド