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

[インデックス 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の現在の値が返される
}

この機能は、特に複数の戻り値がある場合にコードの可読性を高めるのに役立ちます。関数内でsumdiffといった変数を明示的に宣言する必要がなく、returnステートメントで値を指定しない場合、それらの変数の現在の値が関数の戻り値となります。

Goのレース検出器(Race Detector)

Goには、並行処理におけるデータ競合を検出するための組み込みツールであるレース検出器があります。これは、go run -racego 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): 名前付き戻り値パラメータabを持つ関数です。
  • a = 42: aに初期値を代入します。
  • go func() { _ = a; c <- 1 }(): 新しいgoroutineを起動し、その中でaの値を読み取ります。このgoroutineは、aが戻り値として代入されるのと並行して実行される可能性があります。
  • return a, 10: ここで、aの現在の値が戻り値として返されます。修正前は、このreturnステートメントが内部的にa = aのような自己代入を生成し、それがgoroutine内の_ = aとの間でデータ競合を引き起こしていました。

このテストは、修正が適用されたコンパイラではデータ競合が報告されないことを確認します。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント(名前付き戻り値パラメータ、レース検出器に関する情報)
  • Goコンパイラのソースコード(src/cmd/gc/walk.cの関連部分)
  • Go Issue #4014の議論スレッド