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

[インデックス 16565] ファイルの概要

このコミットは、Goコンパイラ(cmd/gc)におけるforループの競合検出(race instrumentation)の挙動を修正するものです。具体的には、forループのテスト式(ntest)とインクリメント式(nincr)のインストゥルメンテーションが、それぞれの初期化リスト(ninit)に適切に配置されるように変更されました。これにより、forループにおけるデータ競合の検出精度が向上しました。

コミット

commit 591d58a3bb9ea3afea0c898564d972b822212674
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Thu Jun 13 16:03:58 2013 +0400

    cmd/gc: properly race-instrument for loops
    Instrumentation of ntest expression should go to ntest->init.
    Same for nincr.
    Fixes #5340.
    
    R=golang-dev, daniel.morsing
    CC=golang-dev
    https://golang.org/cl/10026046

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/591d58a3bb9ea3afea0c898564d972b822212674

元コミット内容

cmd/gc: forループを適切に競合検出インストゥルメントする。 ntest式のインストゥルメンテーションはntest->initに配置されるべきである。 nincrについても同様である。 Issue #5340 を修正する。

変更の背景

Go言語には、並行処理におけるデータ競合を検出するための「Race Detector」というツールが組み込まれています。このツールは、コンパイル時にコードに特別なインストゥルメンテーション(計測コード)を挿入することで機能します。しかし、従来のGoコンパイラ(cmd/gc)では、forループの構造、特にそのテスト式(ループの継続条件)やインクリメント式(ループ変数の更新)に対するインストゥルメンテーションが不適切でした。

具体的には、forループのntest(テスト式)やnincr(インクリメント式)に関連する競合検出のためのコードが、本来配置されるべき場所(それぞれの式の初期化リスト)ではなく、異なる場所に挿入されていました。この誤った配置により、forループ内で発生する可能性のあるデータ競合が正しく検出されない、あるいは誤検出が発生する可能性がありました。

この問題は、GoのIssue #5340として報告されており、このコミットはその問題を解決するために作成されました。目的は、forループの各構成要素が正しくインストゥルメントされ、Race Detectorがより正確に機能するようにすることです。

前提知識の解説

Go Race Detector

Go Race Detectorは、Goプログラムの実行中にデータ競合を検出するための強力なツールです。データ競合は、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みであり、かつそれらのアクセスが同期メカニズムによって保護されていない場合に発生します。データ競合は、プログラムの予測不能な動作やクラッシュの原因となることがあり、デバッグが非常に困難です。

Race Detectorは、コンパイル時にGoコンパイラによってコードに特別なインストゥルメンテーションコードを挿入することで動作します。このインストゥルメンテーションコードは、メモリへの読み書き操作を監視し、競合のパターンを検出します。Race Detectorを有効にするには、go run -racego build -race、またはgo test -raceのように-raceフラグを使用します。

GoコンパイラのASTとノード

Goコンパイラは、ソースコードを解析する際に、抽象構文木(Abstract Syntax Tree, AST)を構築します。ASTは、プログラムの構造を木構造で表現したものです。ASTの各ノードは、変数宣言、関数呼び出し、制御構造(ifforなど)といったプログラムの要素に対応します。

このコミットで言及されているNode構造体は、ASTの基本的な構成要素です。ninitntestnincrといったフィールドは、特定の種類のノード(例えばforループを表すノード)が持つサブノードや関連する初期化リストを指します。

  • ninit: ノードに関連付けられた初期化ステートメントのリスト。例えば、ifステートメントの条件式内で変数を宣言する場合、その宣言はninitリストに含まれます。
  • ntest: forループの継続条件を表す式。
  • nincr: forループの各イテレーションの終わりに実行されるインクリメント式。

コンパイラにおけるインストゥルメンテーション

コンパイラにおけるインストゥルメンテーションとは、プログラムの実行時の振る舞いを監視したり、特定の機能(この場合は競合検出)を有効にしたりするために、コンパイル時に追加のコードを挿入するプロセスを指します。Race Detectorの場合、コンパイラはメモリアクセス操作の前後に追加の関数呼び出しを挿入し、これらの呼び出しがRace Detectorランタイムに情報を提供します。

技術的詳細

このコミットの技術的な核心は、Goコンパイラのsrc/cmd/gc/racewalk.cファイルにおけるracewalknode関数の修正にあります。racewalknode関数は、ASTを走査し、競合検出のためのインストゥルメンテーションを挿入する役割を担っています。

以前の実装では、forループのntest(テスト式)とnincr(インクリメント式)のインストゥルメンテーションが、ループノード自体のninitリストに連結されていました。これは、これらの式の副作用がループの本体とは独立して評価されるべきであるというRace Detectorの要件と矛盾していました。Race Detectorは、各式の評価が独立して監視されることを期待します。

このコミットの主な変更点は、ntestnincrのインストゥルメンテーションを、それぞれ自身のninitリストに直接配置するように修正したことです。これにより、これらの式の評価がより正確に追跡され、競合検出の精度が向上します。

また、この変更のレビュープロセス中に、for iface(t).Foo().b {}のようなインターフェース変換を含むforループの条件式が、コンパイラを無限ループに陥らせるという重大なバグが発見されました。これは、racewalknodeが自身のninitリストを処理する際のロジックに起因していました。具体的には、init == &n->ninitという条件が真の場合、racewalknodeが自身にninitを再帰的に追加しようとする可能性があり、これが無限ループを引き起こしていました。

このバグを修正するため、コミットではracewalknode関数内でinit == &n->ninitの場合の特殊な処理が追加されました。この処理では、まずn->ninitを一時的にnilにし、その内容を別途racewalklistで処理した後、racewalknodeを再帰的に呼び出し、最後に元のn->ninitの内容をappendinitで戻すという手順を踏んでいます。これにより、自己参照による無限ループが回避されます。

さらに、n->right(二項演算子などの右オペランド)のインストゥルメンテーションについても同様の修正が行われ、n->right->ninitに直接インストゥルメンテーションが配置されるようになりました。

これらの変更により、forループの各部分がより独立して、かつ正確に競合検出の対象となるようになり、Race Detectorの信頼性が向上しました。

コアとなるコードの変更箇所

src/cmd/gc/racewalk.cracewalknode関数が主な変更箇所です。

  1. init == &n->ninit の特殊処理の追加:

    -	if(init == nil || init == &n->ninit)
    +	if(init == nil)
     		fatal("racewalk: bad init list");
    +	if(init == &n->ninit) {
    +		// If init == &n->ninit and n->ninit is non-nil,
    +		// racewalknode might append it to itself.
    +		// nil it out and handle it separately before putting it back.
    +		l = n->ninit;
    +		n->ninit = nil;
    +		racewalklist(l, nil);
    +		racewalknode(&n, &l, wr, skip);  // recurse with nil n->ninit
    +		appendinit(&n, l);
    +		*np = n;
    +		return;
    +	}
    

    この変更は、racewalknodeが自身のninitリストを処理する際に発生する可能性のある無限ループを防ぐためのものです。

  2. n->right のインストゥルメンテーションの修正:

    -		// 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;
    -		n->right->ninit = nil;
    -		racewalklist(l, nil);
    -		racewalknode(&n->right, &l, wr, 0);
    -		appendinit(&n->right, l);
    +		racewalknode(&n->right, &n->right->ninit, wr, 0);
    

    n->rightのインストゥルメンテーションが、そのninitに直接関連付けられるように変更されました。

  3. ntestnincr のインストゥルメンテーションの修正:

    -	l = nil;
    -	racewalknode(&n->ntest, &l, 0, 0);
    -	n->ninit = concat(n->ninit, l);
    -	l = nil;
    -	racewalknode(&n->nincr, &l, 0, 0);
    -	n->ninit = concat(n->ninit, l);
    +	racewalknode(&n->ntest, &n->ntest->ninit, 0, 0);
    +	racewalknode(&n->nincr, &n->nincr->ninit, 0, 0);
    

    ntestnincrのインストゥルメンテーションが、それぞれ自身のninitリストに直接配置されるように変更されました。

また、以下のテストファイルが追加・更新されています。

  • src/pkg/runtime/race/testdata/mop_test.go: TestRaceForInit, TestNoRaceForInit, TestRaceForTest, TestRaceForIncr, TestNoRaceForIncrなどの新しいテストケースが追加され、forループの様々な側面における競合検出の挙動が検証されています。
  • src/pkg/runtime/race/testdata/regression_test.go: TestNoRaceForInfiniteLoopという新しいテストケースが追加され、インターフェース変換を含むforループの条件式が無限ループを引き起こさないことを確認しています。

コアとなるコードの解説

このコミットの核心は、racewalknode関数がASTノードを走査し、競合検出のためのインストゥルメンテーションを挿入するロジックの改善にあります。

以前のバージョンでは、forループのntest(テスト式)とnincr(インクリメント式)のインストゥルメンテーションは、ループノード自体のninitリストに集約されていました。これは、Race Detectorが各式の評価を独立して監視する必要があるという要件を満たしていませんでした。例えば、ntest式が副作用を持ち、それが別のゴルーチンと競合する場合、その競合はntest式の評価時に検出されるべきです。しかし、インストゥルメンテーションがループノードのninitにまとめられていると、正確なタイミングでの検出が困難になる可能性がありました。

新しいコードでは、racewalknode(&n->ntest, &n->ntest->ninit, 0, 0);racewalknode(&n->nincr, &n->nincr->ninit, 0, 0); のように、ntestnincrのインストゥルメンテーションが、それぞれ自身のninitリストに直接配置されるようになりました。これにより、これらの式の評価がより正確に追跡され、Race Detectorがより粒度の高い競合検出を行えるようになります。

また、init == &n->ninit の特殊処理は、コンパイラが無限ループに陥るという深刻なバグを修正するためのものです。このバグは、racewalknodeがノードのninitリストを処理する際に、そのノード自体がninitリストの要素として含まれている場合に発生しました。これは、再帰的な処理が無限に続く原因となります。修正されたコードでは、このような自己参照のシナリオを検出し、ninitを一時的にnilにしてから処理し、その後で元の内容を安全に戻すことで、無限ループを回避しています。この修正は、コンパイラの安定性と堅牢性を確保するために不可欠でした。

これらの変更は、GoのRace Detectorの正確性と信頼性を向上させるとともに、コンパイラ自体の安定性も高める重要な改善です。

関連リンク

参考にした情報源リンク