[インデックス 16541] ファイルの概要
コミット
このコミットは、Goコンパイラ(cmd/gc
)がレース検出器(race detector)を有効にしてコンパイルする際に、循環リスト(circular lists)の生成を回避するための修正です。具体的には、条件式(&&
や||
)の右辺の初期化リストが、その右辺自身に再帰的に追加されることで発生する無限ループの問題を解決します。この問題は、GoのIssue 5431として報告されていました。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e7657de7177f92207b2a4f601996529bf415e3f2
元コミット内容
commit e7657de7177f92207b2a4f601996529bf415e3f2
Author: Daniel Morsing <daniel.morsing@gmail.com>
Date: Tue Jun 11 21:19:29 2013 +0200
cmd/gc: avoid creating circular lists when compiling with race detector.
Fixes #5431.
R=dvyukov, remyoudompheng, rsc
CC=gobot, golang-dev
https://golang.org/cl/9910043
---
src/cmd/gc/racewalk.c | 6 +++++-\n
src/pkg/runtime/race/testdata/regression_test.go | 15 +++++++++++++++\n
2 files changed, 20 insertions(+), 1 deletion(-)\n
変更の背景
この変更は、Goコンパイラがレース検出器を有効にしてコードをコンパイルする際に発生する特定のバグ(Go Issue 5431)を修正するために導入されました。このバグは、論理AND (&&
) や論理OR (||
) のような短絡評価される式において、右辺の式が持つ初期化リスト(ninit
)が、その右辺自身に循環的に追加されてしまうことで発生しました。
具体的には、racewalknode
関数がAST(抽象構文木)を走査し、レース検出のためのインストゥルメンテーション(計測コードの挿入)を行う際に、n->right->ninit
が非nil
の場合、racewalknode
がそれを自身に再帰的に追加しようとすることが問題でした。これにより、コンパイラが無限ループに陥り、コンパイルが完了しない、あるいはスタックオーバーフローが発生するなどの問題を引き起こしていました。
この問題は、特にインライン化された関数呼び出しが絡む複雑な条件式で顕著に現れました。レース検出器は、メモリへのアクセスを監視するために追加のコードを挿入するため、ASTの構造を通常よりも複雑にする可能性があり、このようなエッジケースが露呈しやすくなります。
前提知識の解説
このコミットを理解するためには、以下の概念について理解しておく必要があります。
- Goコンパイラ (
cmd/gc
): Go言語の公式コンパイラです。Goのソースコードを機械語に変換する役割を担います。コンパイル過程では、ソースコードをAST(抽象構文木)に変換し、様々な最適化やコード生成を行います。 - レース検出器 (Race Detector): Goランタイムに組み込まれているツールで、並行処理におけるデータ競合(data race)を検出します。データ競合は、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みであり、かつそれらのアクセスが同期メカニズムによって保護されていない場合に発生します。レース検出器は、コンパイル時に追加のインストゥルメンテーションコードを挿入することで、実行時にデータ競合を監視します。
- 抽象構文木 (Abstract Syntax Tree, AST): プログラムのソースコードの抽象的な構文構造を木構造で表現したものです。コンパイラはASTを操作して、コードの解析、変換、最適化を行います。Goコンパイラでは、各ノードが式、文、宣言などを表します。
Node
構造体とninit
フィールド: Goコンパイラの内部では、ASTの各要素がNode
構造体として表現されます。Node
構造体には、そのノードに関連する初期化文のリストを保持するためのninit
フィールドがあります。これは、特定の式や変数が使用される前に実行されるべき副作用のある操作(例:関数呼び出し、変数宣言)を管理するために使用されます。- 短絡評価 (Short-circuit evaluation): 論理AND (
&&
) や論理OR (||
) のような論理演算子において、式の評価が途中で打ち切られることです。A && B
:A
がfalse
であれば、B
は評価されません。A || B
:A
がtrue
であれば、B
は評価されません。 レース検出器は、評価される可能性のあるすべてのコードパスにインストゥルメンテーションを挿入する必要があるため、短絡評価の特性を考慮する必要があります。
- 循環リスト (Circular List): リストの最後の要素が最初の要素を指すなど、リストの要素が循環的に参照し合う構造です。プログラミングにおいて、意図しない循環参照は無限ループやメモリリークの原因となることがあります。
技術的詳細
このコミットの核心は、src/cmd/gc/racewalk.c
ファイル内のracewalknode
関数における変更です。racewalknode
は、ASTノードを走査し、レース検出器のためのインストゥルメンテーションコードを挿入する役割を担っています。
問題が発生していたのは、論理AND (ONAND
) や論理OR (ONOR
) のような短絡評価される演算子を処理する部分でした。これらの演算子では、左辺の評価結果に基づいて右辺が実行されるかどうかが決まります。レース検出器は、右辺が実行される可能性のある場合に、その右辺にもインストゥルメンテーションを適用する必要があります。
元のコードでは、n->right
(右辺のASTノード)の処理を行う際に、n->right->ninit
(右辺の初期化リスト)を直接l
に代入し、そのl
をracewalknode
の引数として渡していました。その後、racewalknode
の呼び出しから戻った後、appendinit(&n->right, l)
によってl
の内容をn->right
の初期化リストに追加していました。
ここで問題となるのは、racewalknode
がl
を引数として受け取り、その内部でl
にノードを追加する可能性があることです。もしn->right->ninit
が非nil
で、かつracewalknode
がl
にn->right->ninit
自身を(あるいはその一部を)追加してしまうと、n->right->ninit
が自身を参照する循環リストが形成されてしまいます。これは、appendinit
がリストを結合する際に無限ループを引き起こす原因となります。
この修正では、この循環参照を防ぐために以下の手順を踏んでいます。
l = n->right->ninit;
で、まず右辺の初期化リストを一時変数l
に保存します。n->right->ninit = nil;
で、元の右辺の初期化リストを一時的にnil
に設定します。これが最も重要な変更点です。これにより、racewalknode(&n->right, &l, wr, 0)
が呼び出された際に、n->right
が自身のninit
を再帰的に参照することを防ぎます。racewalklist(l, nil);
を呼び出し、一時変数l
に保存された元の初期化リストを先に処理します。これにより、そのリスト内のノードが適切にインストゥルメンテーションされます。racewalknode(&n->right, &l, wr, 0);
を呼び出し、右辺のノード自体を走査し、その結果として生成される初期化リストをl
に追加します。appendinit(&n->right, l);
で、最終的にl
に集められたすべての初期化ノードを、n->right
の初期化リストに結合します。
この変更により、racewalknode
が処理する際に、n->right->ninit
が一時的に切り離されるため、循環参照が発生する可能性がなくなります。
コアとなるコードの変更箇所
変更は主にsrc/cmd/gc/racewalk.c
ファイルにあります。
--- a/src/cmd/gc/racewalk.c
+++ b/src/cmd/gc/racewalk.c
@@ -255,7 +255,11 @@ racewalknode(Node **np, NodeList **init, int wr, int skip)\n
// side effects are safe.\n
// n->right may not be executed,\n
// so instrumentation goes to n->right->ninit, not init.\n
-\t\tl = nil;\n
+\t\t// If right->ninit is non-nil, racewalknode might append it to itself.\n
+\t\t// nil it out and handle it separately before putting it back.\n
+\t\tl = n->right->ninit;\n
+\t\tn->right->ninit = nil;\n
+\t\tracewalklist(l, nil);\n
\t\tracewalknode(&n->right, &l, wr, 0);\n
\t\tappendinit(&n->right, l);\n
\t\tgoto ret;
また、この修正が正しく機能することを確認するための回帰テストが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
@@ -160,3 +160,18 @@ func noRaceReturn(c chan int) (a, b int) {\n
}()\n
return a, 10\n
}\n+\n+func issue5431() {\n+\tvar p **inltype\n+\tif inlinetest(p).x && inlinetest(p).y {\n+\t} else if inlinetest(p).x || inlinetest(p).y {\n+\t}\n+}\n+\n+type inltype struct {\n+\tx, y bool\n+}\n+\n+func inlinetest(p **inltype) *inltype {\n+\treturn *p\n+}\n```
## コアとなるコードの解説
`racewalk.c`の変更点に焦点を当てて解説します。
変更前のコード:
```c
// n->right may not be executed,
// so instrumentation goes to n->right->ninit, not init.
l = nil; // ここでlをnilに初期化
racewalknode(&n->right, &l, wr, 0);
appendinit(&n->right, l);
goto ret;
このコードでは、n->right
の処理に入る前にl
をnil
に初期化していました。しかし、n->right
自体が既にninit
リストを持っている場合、そのリストの内容が失われるか、あるいはracewalknode
の内部でl
にn->right->ninit
が追加され、その結果として循環参照が発生する可能性がありました。特に、n->right
が持つninit
リストが、racewalknode
の処理中にl
に渡され、そのl
が再びn->right
のninit
に結合される際に問題が生じます。
変更後のコード:
// If right->ninit is non-nil, racewalknode might append it to itself.
// nil it out and handle it separately before putting it back.
l = n->right->ninit; // 1. n->rightの既存のninitをlに保存
n->right->ninit = nil; // 2. n->rightのninitを一時的にnilにする
racewalklist(l, nil); // 3. 保存したl(元のninit)を先に処理する
racewalknode(&n->right, &l, wr, 0); // 4. n->rightを走査し、新たな初期化ノードをlに追加
appendinit(&n->right, l); // 5. lの全ての内容をn->rightのninitに結合
goto ret;
この修正の肝は、n->right->ninit = nil;
の行です。これにより、racewalknode(&n->right, &l, wr, 0)
が呼び出される際に、n->right
が自身のninit
を既に持っている状態ではなくなります。つまり、racewalknode
がn->right
を処理する際に、そのノードが持つべき初期化リストは、l
を通じてのみ提供されるようになります。
元のn->right->ninit
はl
に保存され、racewalklist(l, nil)
によって先に処理されます。これは、元の初期化リストに含まれる式もレース検出器のインストゥルメンテーションの対象となるためです。その後、racewalknode
がn->right
を処理し、その過程で生成される新たな初期化ノードがl
に追加されます。最後に、appendinit
によって、元の初期化ノードと新しく生成された初期化ノードがすべてn->right
のninit
に結合されます。
この一連の操作により、n->right
のninit
が自身を再帰的に参照するような循環構造が形成されることがなくなり、コンパイラの無限ループが解消されます。
regression_test.go
に追加されたテストケースissue5431()
は、この問題が短絡評価される論理演算子(&&
と||
)と、インライン化される可能性のある関数呼び出し(inlinetest
)の組み合わせで発生することを示しています。このテストは、修正が正しく機能し、コンパイラがこの種のコードを正常に処理できることを保証します。
関連リンク
- Go Issue 5431: https://github.com/golang/go/issues/5431
- Go CL 9910043: https://golang.org/cl/9910043
参考にした情報源リンク
- Go言語の公式ドキュメント
- Goコンパイラのソースコード
- Go Issue Tracker
- Go Code Reviewサイト (Gerrit)
- データ競合に関する一般的な情報源 (例: Wikipedia, 並行プログラミングの書籍)
- AST (抽象構文木) に関する一般的な情報源
- 短絡評価に関する一般的な情報源
- 循環リストに関する一般的な情報源
- Go Race Detectorに関する公式ブログ記事やドキュメント
- The Go Race Detector: https://go.dev/blog/race-detector
- A Tour of Go: Concurrency (Data Races): https://go.dev/tour/concurrency/9
- Goコンパイラの内部構造に関する資料 (例: "Go Compiler Internals" などのプレゼンテーションや記事)
- Goの
cmd/gc
に関するソースコードコメントと関連するコミット履歴 - Goの
Node
構造体とninit
フィールドに関するGoコンパイラソースコード内の定義と使用箇所 racewalknode
およびracewalklist
関数の実装詳細appendinit
関数の実装詳細- Goのテストフレームワークと回帰テストの記述方法に関する情報