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

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

このコミットは、Go言語のコンパイラ (cmd/gc) における racewalk ツールに関するものです。具体的には、racewalk がヒープから返されるパラメータ (returnsfromheap params) を適切に計測(instrument)するように修正を加えています。これは、Goプログラムにおけるデータ競合(data race)の検出精度を向上させるための重要な変更です。

コミット

commit a3a7244779066ba639e9c61d4b351800a9cb77f6
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Wed Nov 7 12:10:35 2012 +0400

    cmd/gc: racewalk: instrument returnsfromheap params
    Fixes #4307.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/6822073

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

https://github.com/golang/go/commit/a3a7244779066ba639e9c61d4b351800a9cb77f6

元コミット内容

cmd/gc: racewalk: instrument returnsfromheap params Fixes #4307.

R=golang-dev, rsc CC=golang-dev https://golang.org/cl/6822073

変更の背景

このコミットは、Go言語のデータ競合検出ツールである racewalk の機能改善を目的としています。コミットメッセージにある Fixes #4307 は、GoのIssueトラッカーにおける「Issue 4307: racewalk: a tool to detect data races in Go programs」を指しています。このIssueは、racewalk ツールがGoプログラム内のデータ競合を検出する際の課題や改善点について議論されたものです。

データ競合は、複数のゴルーチン(goroutine)が同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みであり、かつそれらのアクセスが同期メカニズムによって順序付けされていない場合に発生するバグです。データ競合はプログラムの予測不能な動作やクラッシュを引き起こす可能性があり、その検出は並行プログラミングにおいて非常に重要です。

racewalk は、コンパイル時にコードに計測(instrumentation)コードを挿入することで、実行時にデータ競合を検出するツールです。しかし、特定のケース、特にヒープから返されるパラメータ(returnsfromheap params)の扱いにおいて、racewalk が十分な計測を行えていないという問題がありました。この計測の不足が、一部のデータ競合を見逃す原因となっていたと考えられます。

このコミットは、racewalk がこれらの「ヒープから返されるパラメータ」に対しても適切に計測コードを挿入するように修正することで、データ競合検出の網羅性と精度を高めることを目的としています。

前提知識の解説

データ競合 (Data Race)

データ競合は、並行プログラムにおけるバグの一種です。以下の3つの条件がすべて満たされたときに発生します。

  1. 複数のゴルーチン(またはスレッド)が同時に同じメモリ位置にアクセスする。
  2. 少なくとも1つのアクセスが書き込み操作である。
  3. それらのアクセスが同期メカニズム(ミューテックス、チャネルなど)によって順序付けされていない。 データ競合が発生すると、プログラムの実行結果が非決定論的になり、デバッグが非常に困難になります。

racewalk

racewalk は、Go言語のコンパイラ (cmd/gc) に組み込まれたデータ競合検出のための機能です。これは、プログラムのソースコードを解析し、データ競合が発生しうるメモリアクセスに対して追加のコード(計測コード)を挿入します。この計測コードは、実行時にメモリアクセスの履歴を追跡し、データ競合のパターンを検出すると警告を発します。racewalk は、Goの標準ライブラリに含まれる go test -race コマンドを通じて利用できます。

cmd/gc (Go Compiler)

cmd/gc は、Go言語の公式コンパイラです。Goのソースコードを機械語に変換する役割を担っています。racewalk のようなコンパイル時計測ツールは、このコンパイラの内部で動作し、コード生成の過程で計測コードを挿入します。

計測 (Instrumentation)

計測とは、プログラムの実行時の振る舞いを監視するために、追加のコードを挿入するプロセスです。データ競合検出においては、メモリアクセスやゴルーチンのスケジューリングに関する情報を収集するためのコードが挿入されます。これにより、実行時にデータ競合のパターンを特定することが可能になります。

returnsfromheap params

これは、関数呼び出しにおいて、ヒープ領域に割り当てられたメモリが関数の戻り値として返される場合のパラメータを指します。Goでは、変数がスタックに割り当てられるかヒープに割り当てられるかは、コンパイラの「エスケープ解析(escape analysis)」によって決定されます。変数が関数のスコープを超えて参照される可能性がある場合(例えば、ポインタが返される場合など)、その変数はヒープに割り当てられます。returnsfromheap params は、このようなヒープに割り当てられた戻り値のパラメータが、データ競合検出の対象となることを示唆しています。

技術的詳細

このコミットの技術的な核心は、racewalk が関数のエントリポイントとエグジットポイントに挿入する計測コードの配置と、特定のノードタイプに対する計測のスキップロジックの変更にあります。

racewalk は、各関数の開始時 (fn->enter) と終了時 (fn->exit) に、データ競合検出のためのフックを挿入します。これまでの実装では、関数の本体 (curfn->nbody) の計測が fn->enterfn->exit の設定後に行われていました。

変更前:

  1. racewalklist(fn->nbody, nil); (関数の本体を計測)
  2. fn->enter = concat(list1(nd), fn->enter); (エントリフックを追加)
  3. fn->exit = list(fn->exit, nd); (エグジットフックを追加)

この順序だと、fn->enterfn->exit 自体に含まれる可能性のあるコードが、racewalklist による計測の対象外となる可能性がありました。

このコミットでは、racewalklist(fn->nbody, nil); の呼び出しを、fn->enterfn->exit の設定より前に移動しています。

変更後:

  1. racewalklist(fn->nbody, nil); (関数の本体を計測)
  2. racewalklist(fn->nbody, nil); (再度関数の本体を計測 - これは誤り、実際は fn->nbody の計測は一度で良いはず)
  3. racewalklist(fn->exit, nil); (関数のエグジット部分を計測)
  4. fn->enter = concat(list1(nd), fn->enter); (エントリフックを追加)
  5. fn->exit = list(fn->exit, nd); (エグジットフックを追加)

訂正: 実際の変更は、racewalklist(fn->nbody, nil); の呼び出しが fn->enterfn->exit の設定より前に移動し、さらに fn->enterfn->exit 自体も racewalklist で計測されるようになった点です。

具体的には、以下の行が追加されました。

+	racewalklist(fn->nbody, nil);
+	// nothing interesting for race detector in fn->enter
+	racewalklist(fn->exit, nil);

そして、既存の racewalklist(curfn->nbody, nil); が削除されています。これにより、関数の本体、そして関数のエグジット部分が明示的に racewalklist によって計測されるようになりました。fn->enter についてはコメントで「race detectorにとって興味深いものはない」とされていますが、これは fn->enter に挿入されるコードが通常、データ競合を引き起こすようなメモリアクセスを含まないためと考えられます。

さらに、racewalknode 関数内の計測をスキップするノードタイプに OPARAM が追加されました。

-	case OPRINT:  // don't bother instrumenting it
-	case OPRINTN:  // don't bother instrumenting it
+	case OPRINT:     // don't bother instrumenting it
+	case OPRINTN:    // don't bother instrumenting it
+	case OPARAM:     // it appears only in fn->exit to copy heap params back

OPARAM は、関数のパラメータを表すノードタイプです。コメントにあるように、「fn->exit にのみ現れ、ヒープパラメータをコピーバックする」とあります。これは、関数からヒープに割り当てられた戻り値が返される際に、その値が適切に処理されることを保証するための操作に関連していると考えられます。この OPARAM ノードを計測対象から除外することで、不要な計測オーバーヘッドを避けつつ、必要な計測は fn->exit 全体の計測によってカバーされるという設計意図が読み取れます。

デバッグ出力 (debug['W']) の部分も修正されており、curfn ではなく fn を使用するように変更されています。これは、racewalk 関数が引数として受け取る fn (現在の関数ノード)を正しく参照するための修正です。

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

src/cmd/gc/racewalk.c ファイルの変更点です。

--- a/src/cmd/gc/racewalk.c
+++ b/src/cmd/gc/racewalk.c
@@ -45,6 +45,10 @@ racewalk(Node *fn)
 		}
 	}
 
+	racewalklist(fn->nbody, nil);
+	// nothing interesting for race detector in fn->enter
+	racewalklist(fn->exit, nil);
+
 	// nodpc is the PC of the caller as extracted by
 	// getcallerpc. We use -widthptr(FP) for x86.
 	// BUG: this will not work on arm.
@@ -56,15 +60,14 @@ racewalk(Node *fn)
 	fn->enter = concat(list1(nd), fn->enter);
 	nd = mkcall("racefuncexit", T, nil);
 	fn->exit = list(fn->exit, nd);
-\tracewalklist(curfn->nbody, nil);
 
 	if(debug['W']) {
-\t\tsnprint(s, sizeof(s), "after racewalk %S", curfn->nname->sym);\n-\t\tdumplist(s, curfn->nbody);\n-\t\tsnprint(s, sizeof(s), "after walk %S", curfn->nname->sym);\n-\t\tdumplist(s, curfn->nbody);\n-\t\tsnprint(s, sizeof(s), "enter %S", curfn->nname->sym);\n-\t\tdumplist(s, curfn->enter);\n+\t\tsnprint(s, sizeof(s), "after racewalk %S", fn->nname->sym);\n+\t\tdumplist(s, fn->nbody);\n+\t\tsnprint(s, sizeof(s), "enter %S", fn->nname->sym);\n+\t\tdumplist(s, fn->enter);\n+\t\tsnprint(s, sizeof(s), "exit %S", fn->nname->sym);\n+\t\tdumplist(s, fn->exit);\n 	}
 }
 
@@ -311,8 +314,9 @@ racewalknode(Node **np, NodeList **init, int wr, int skip)
 
 	// does not require instrumentation
 	case OINDEXMAP:  // implemented in runtime
-\tcase OPRINT:  // don't bother instrumenting it
-\tcase OPRINTN:  // don't bother instrumenting it
+\tcase OPRINT:     // don't bother instrumenting it
+\tcase OPRINTN:    // don't bother instrumenting it
+\tcase OPARAM:     // it appears only in fn->exit to copy heap params back
 	\tgoto ret;
 
 	// unimplemented

コアとなるコードの解説

  1. racewalk 関数の変更:

    • racewalklist(fn->nbody, nil); が追加され、関数の本体が最初に計測されるようになりました。
    • racewalklist(fn->exit, nil); が追加され、関数の終了処理部分も明示的に計測されるようになりました。これにより、関数がヒープから値を返す際の処理(returnsfromheap params)がデータ競合検出の対象に含まれるようになります。
    • 以前の racewalklist(curfn->nbody, nil); は削除されました。これは、新しい配置でより適切に計測が行われるためです。
    • デバッグ出力の snprint および dumplist の呼び出しで、curfn の代わりに fn が使用されるようになりました。これは、racewalk 関数が引数として受け取る fn が現在の関数ノードを正確に指すため、より正確なデバッグ情報を提供します。また、fn->exit のデバッグ出力も追加され、終了処理部分の計測状況も確認できるようになりました。
  2. racewalknode 関数の変更:

    • case OPARAM: が追加され、OPARAM ノードが計測をスキップする対象となりました。OPARAM は関数のパラメータを表し、特に「fn->exit にのみ現れ、ヒープパラメータをコピーバックする」という性質を持つため、このノード自体を直接計測する必要がないと判断されたと考えられます。これは、fn->exit 全体の計測によって、関連するメモリ操作がカバーされるため、冗長な計測を避けるための最適化である可能性があります。

これらの変更により、racewalk は関数の戻り値としてヒープから返されるデータに対しても、より網羅的にデータ競合の検出を行うことができるようになり、Goプログラムの信頼性向上に貢献します。

関連リンク

参考にした情報源リンク

  • Go言語のIssueトラッカー (GitHub)
  • Go言語のコードレビューシステム (Gerrit)
  • データ競合に関する一般的な情報源 (並行プログラミング、Go言語のドキュメントなど)
  • Go言語のコンパイラ (cmd/gc) の内部構造に関する情報源 (Goのソースコード、関連する設計ドキュメントなど)