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

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

このコミットは、Goコンパイラのcmd/gcパッケージ内のレース検出器(race detector)関連のコード、特にracewalk.cファイルにおけるいくつかのマイナーな問題を修正するものです。主な目的は、レース検出器の正確性とデバッグ能力を向上させることにあります。具体的には、関数のエントリポイントでのレース検出器フックの挿入順序の修正、デバッグ情報の強化、型スイッチ式における潜在的なメモリアクセスの考慮、そしてブランク識別子(_)の扱いに関する修正が含まれています。

コミット

commit de10a23db14106588b608d724510849c4e5f278a
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Thu Nov 1 22:56:04 2012 +0400

    cmd/gc: racewalk: fix a bunch of minor issues
    1. Prepend racefuncenter() to fn->enter -- fn->enter can contain new() calls,
    and we want them to be in the scope of the function.
    2. Dump fn->enter and fn->exit.
    3. Add TODO that OTYPESW expression can contain interesting memory accesses.
    4. Ignore only _ names instead of all names starting with _.
    
    R=golang-dev, rsc
    CC=golang-dev
    https://golang.org/cl/6822048

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

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

元コミット内容

cmd/gc: racewalk: fix a bunch of minor issues
1. Prepend racefuncenter() to fn->enter -- fn->enter can contain new() calls,
and we want them to be in the scope of the function.
2. Dump fn->enter and fn->exit.
3. Add TODO that OTYPESW expression can contain interesting memory accesses.
4. Ignore only _ names instead of all names starting with _.

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

変更の背景

このコミットは、Goのレース検出器(race detector)の内部実装におけるいくつかの細かな不具合と改善点を修正するために行われました。レース検出器は、並行プログラムにおけるデータ競合(data race)を検出するための重要なツールであり、その正確性と効率性はGoプログラムの信頼性を保証する上で不可欠です。

具体的な背景としては、以下の点が挙げられます。

  1. racefuncenterの挿入タイミング: racefuncenterは、関数のエントリ時に呼び出されるレース検出器のフック関数です。このフックが関数の初期化コード(特にnew()のようなメモリ割り当てを行う呼び出し)よりも後に挿入されると、これらの初期化処理がレース検出器の監視範囲外になってしまう可能性がありました。これを修正し、関数のスコープ全体でレース検出器が有効になるように、racefuncenterの呼び出しをfn->enterリストの先頭に移動する必要がありました。
  2. デバッグ情報の強化: コンパイラのデバッグ時には、関数のエントリ(fn->enter)とエグジット(fn->exit)のコードリストの内容を確認できると、問題の特定が容易になります。このコミットでは、デバッグフラグが有効な場合にこれらのリストの内容をダンプする機能が追加されました。
  3. OTYPESW式の考慮: OTYPESWはGoの型スイッチ(type switch)を表す内部ノードです。型スイッチの式自体が、関数呼び出しやメモリ読み込みといった、レース検出器が監視すべき操作を含む可能性があります。以前の実装では、この可能性が十分に考慮されていなかったため、将来的な改善点としてTODOコメントが追加されました。
  4. ブランク識別子(_)の厳密な無視: Go言語では、ブランク識別子_は値を破棄するために使用されます。レース検出器がコードを解析する際、この_に関連するシンボルは通常無視されるべきです。しかし、以前の実装では、_で始まるすべての名前(例: _myVar)を無視してしまっていました。これは意図しない副作用を引き起こす可能性があったため、厳密に_という名前のみを無視するように修正されました。

これらの修正は、レース検出器の堅牢性を高め、より正確なデータ競合の検出を可能にすることを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGoコンパイラおよびレース検出器に関する前提知識が必要です。

  • Goコンパイラ(cmd/gc: Go言語の公式コンパイラであり、Goソースコードを機械語に変換する役割を担います。cmd/gcは、フロントエンド(構文解析、型チェック)、ミドルエンド(最適化)、バックエンド(コード生成)の各フェーズを持ちます。
  • レース検出器(Race Detector): Go言語に組み込まれているツールで、並行プログラムにおけるデータ競合(data race)を検出します。データ競合とは、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みであり、かつそれらのアクセスが同期メカニズムによって保護されていない場合に発生するバグです。レース検出器は、コンパイル時または実行時にコードにフックを挿入することで、メモリアクセスを監視し、競合を報告します。
  • racewalk.c: Goコンパイラのcmd/gcディレクトリにあるC言語のソースファイルで、レース検出器の主要なロジックが実装されています。このファイルは、Goの抽象構文木(AST)を走査し、レース検出に必要なインストゥルメンテーション(フックの挿入)を行います。
  • 抽象構文木(AST)とNodeNodeList: コンパイラはソースコードを解析して抽象構文木(AST)を構築します。ASTはプログラムの構造を木構造で表現したものです。NodeはASTの各ノード(式、文、宣言など)を表すデータ構造であり、NodeListNodeのリストです。
  • fn->enterfn->exit: Goコンパイラの内部では、各関数(fn)は、その関数のエントリ時に実行されるべきコードのリスト(fn->enter)と、エグジット時に実行されるべきコードのリスト(fn->exit)を持つことができます。レース検出器は、これらのリストにracefuncenterracefuncexitといったフック関数を挿入します。
  • mkcall: Goコンパイラの内部関数で、指定された関数を呼び出すためのASTノード(Node)を作成します。
  • OTYPESW: Goコンパイラの内部で定義されているASTノードのオペレーションコードの一つで、型スイッチ(switch v.(type))を表します。
  • ONAME: ASTノードのオペレーションコードの一つで、名前(変数、関数名など)を表します。
  • TSTRUCT: 型を表す内部構造体で、特に構造体型を示します。
  • sym: シンボル(symbol)を表す内部構造体で、変数名、関数名などの識別子に関連付けられた情報を含みます。
  • ブランク識別子(_: Go言語の特殊な識別子で、値を明示的に破棄するために使用されます。例えば、_ = someFunc()のように、戻り値を無視する場合に用いられます。

技術的詳細

このコミットで行われた技術的な変更は、以下の4つの主要な点に集約されます。

  1. racefuncenter()fn->enterへの挿入順序の変更:

    • 変更前: racefuncenterの呼び出しは、fn->enterリストの末尾に追加されていました(fn->enter = list(fn->enter, nd);)。
    • 問題点: fn->enterには、関数の初期化処理(例: new()によるメモリ割り当て)が含まれる可能性がありました。racefuncenterがこれらの処理の後に挿入されると、初期化処理中のメモリアクセスがレース検出器の監視対象外となるリスクがありました。
    • 変更後: racefuncenterの呼び出しを表すノードndが、fn->enterリストの先頭に挿入されるように変更されました(fn->enter = concat(list1(nd), fn->enter);)。これにより、関数のエントリ時に実行されるすべてのコードがracefuncenterのスコープ内で実行されることが保証され、レース検出の正確性が向上します。
    • コードの差分:
      -	fn->enter = list(fn->enter, nd);
      +	nd = mkcall("racefuncenter", T, nil);
      +	fn->enter = concat(list1(nd), fn->enter);
      
      (注: 元のdiffにはマージコンフリクトマーカーが含まれていましたが、最終的な変更は上記のようにconcatによる先頭への追加です。)
  2. fn->enterfn->exitのデバッグダンプの追加:

    • 変更前: デバッグモード(debug['W']が有効な場合)では、関数のボディ(curfn->nbody)のみがダンプされていました。
    • 変更後: racewalk関数の最後に、fn->enterfn->exitの内容もデバッグ出力としてダンプされるように変更されました。これにより、レース検出器が挿入したフックが正しく配置されているかを確認しやすくなり、デバッグ作業が効率化されます。
    • コードの差分:
      --- a/src/cmd/gc/racewalk.c
      +++ b/src/cmd/gc/racewalk.c
      @@ -59,6 +59,9 @@ racewalk(Node *fn)
       \tif(debug['W']) {
       \t\tsnprint(s, sizeof(s), "after racewalk %S", curfn->nname->sym);
       \t\tdumplist(s, curfn->nbody);
      +\t\tsnprint(s, sizeof(s), "after walk %S", curfn->nname->sym);
      +\t\tdumplist(s, curfn->nbody);
      +\t\tsnprint(s, sizeof(s), "enter %S", curfn->nname->sym);
      +\t\tdumplist(s, curfn->enter);
       \t}
       }
      
  3. OTYPESW式に関するTODOコメントの追加:

    • 変更前: OSWITCH(スイッチ文)のOTYPESW(型スイッチ)ケースでは、「静的型付けがあるため、気にする必要はない」というコメントがありました。
    • 問題点: 型スイッチの式自体が、関数呼び出しやメモリ読み込みといった、レース検出器が監視すべき操作を含む可能性があります。この可能性が以前は十分に考慮されていませんでした。
    • 変更後: 既存のコメントが削除され、代わりに「式が呼び出しや読み込みを含む可能性がある」というTODOコメントが追加されました。これは、将来的にこのケースでのレース検出器の処理を改善する必要があることを示唆しています。
    • コードの差分:
      --- a/src/cmd/gc/racewalk.c
      +++ b/src/cmd/gc/racewalk.c
      @@ -163,7 +175,7 @@ racewalknode(Node **np, NodeList **init, int wr, int skip)
       \n \tcase OSWITCH:
       \t\tif(n->ntest->op == OTYPESW)
      -\t\t\t// don't bother, we have static typization
      +\t\t\t// TODO(dvyukov): the expression can contain calls or reads.
       \t\t\treturn;
       \t\tracewalknode(&n->ntest, &n->ninit, 0, 0);
       \t\tracewalklist(n->nbody, nil);
      
  4. ブランク識別子(_)の無視ロジックの厳密化:

    • 変更前: callinstr関数内で、シンボル名が_で始まる場合にそのシンボルを無視していました(strncmp(n->sym->name, "_", sizeof("_")-1) == 0)。これは、_myVarのような名前も無視してしまう可能性がありました。
    • 問題点: Go言語では、ブランク識別子_は特定の目的(値の破棄など)のために使用され、それ以外の_で始まる名前は通常の識別子として扱われるべきです。strncmpを使用すると、_で始まるすべての名前がブランク識別子として誤って扱われる可能性がありました。
    • 変更後: strncmpstrcmpに変更され、シンボル名が厳密に_である場合にのみ無視されるようになりました(strcmp(n->sym->name, "_") == 0)。これにより、ブランク識別子の扱いがより正確になり、意図しないシンボルの無視が防止されます。同様の修正が、構造体フィールドの走査ロジックにも適用されています。
    • コードの差分:
      --- a/src/cmd/gc/racewalk.c
      +++ b/src/cmd/gc/racewalk.c
      @@ -369,12 +381,12 @@ callinstr(Node *n, NodeList **init, int wr, int skip)
       \tif(n->op == ONAME) {
       \t\tif(n->sym != S) {
       \t\t\tif(n->sym->name != nil) {
      -\t\t\t\tif(strncmp(n->sym->name, "_", sizeof("_")-1) == 0)
      +\t\t\t\tif(strcmp(n->sym->name, "_") == 0)
       \t\t\t\t\treturn 0;
       \t\t\t\tif(strncmp(n->sym->name, "autotmp_", sizeof("autotmp_")-1) == 0)
       \t\t\t\t\treturn 0;
       \t\t\t\t...
       \t\t\t\t...
       \t\t\t\tfor(t1=t->type; t1; t1=t1->down) {
      -\t\t\t\tif(t1->sym && strncmp(t1->sym->name, "_", sizeof("_")-1)) {
      +\t\t\t\tif(t1->sym && strcmp(t1->sym->name, "_")) {
       \t\t\t\t\tn = treecopy(n);
       \t\t\t\t\tf = nod(OXDOT, n, newname(t1->sym));
       \t\t\t\t\tif(callinstr(f, init, wr, 0)) {
      

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

--- a/src/cmd/gc/racewalk.c
+++ b/src/cmd/gc/racewalk.c
@@ -43,19 +43,24 @@ racewalk(Node *fn)
 		}
 	}
 
-<<<<<<< local
-	// TODO(dvyukov): ideally this should be:
-	// racefuncenter(getreturnaddress())
-	// because it's much more costly to obtain from runtime library.
-	nd = mkcall("racefuncenter", T, nil);
-	fn->enter = concat(list1(nd), fn->enter);
-=======
-	// nodpc is the PC of the caller as extracted by
-	// getcallerpc. We use -widthptr(FP) for x86.
-	// BUG: this will not work on arm.
-	// TODO(rsc): this is wrong. getcallerpc is not a function.
-	// It's a pseudo-function that the compiler rewrites.
-	nodpc = nod(ONAME, sysvar("FP"), nil);
-	nodpc->xoffset = -widthptr;
-	nd = mkcall("racefuncenter", T, nil, nodpc);
-	fn->enter = list(fn->enter, nd);
->>>>>>> other
+	// TODO(dvyukov): ideally this should be:
+	// racefuncenter(getreturnaddress())
+	// because it's much more costly to obtain from runtime library.
+	nd = mkcall("racefuncenter", T, nil);
+	fn->enter = concat(list1(nd), fn->enter);
 	nd = mkcall("racefuncexit", T, nil);
-	fn->exit = list(fn->exit, nd); // works fine if (!fn->exit)
+	fn->exit = list(fn->exit, nd);
 	racewalklist(curfn->nbody, nil);
 
 	if(debug['W']) {
 		snprint(s, sizeof(s), "after racewalk %S", curfn->nname->sym);
 		dumplist(s, curfn->nbody);
+		snprint(s, sizeof(s), "after walk %S", curfn->nname->sym);
+		dumplist(s, curfn->nbody);
+		snprint(s, sizeof(s), "enter %S", curfn->nname->sym);
+		dumplist(s, curfn->enter);
 	}
 }
 
@@ -163,7 +168,7 @@ racewalknode(Node **np, NodeList **init, int wr, int skip)
 
 	case OSWITCH:
 		if(n->ntest->op == OTYPESW)
-			// don't bother, we have static typization
+			// TODO(dvyukov): the expression can contain calls or reads.
 			return;
 		racewalknode(&n->ntest, &n->ninit, 0, 0);
 		racewalklist(n->nbody, nil);
@@ -369,12 +374,12 @@ callinstr(Node *n, NodeList **init, int wr, int skip)
 	if(n->op == ONAME) {
 		if(n->sym != S) {
 			if(n->sym->name != nil) {
-				if(strncmp(n->sym->name, "_", sizeof("_")-1) == 0)
+				if(strcmp(n->sym->name, "_") == 0)
 					return 0;
 				if(strncmp(n->sym->name, "autotmp_", sizeof("autotmp_")-1) == 0)
 					return 0;
 				...
 				...
 				for(t1=t->type; t1; t1=t1->down) {
-					if(t1->sym && strncmp(t1->sym->name, "_", sizeof("_")-1)) {
+					if(t1->sym && strcmp(t1->sym->name, "_")) {
 						n = treecopy(n);
 						f = nod(OXDOT, n, newname(t1->sym));
 						if(callinstr(f, init, wr, 0)) {

コアとなるコードの解説

上記のコード変更は、src/cmd/gc/racewalk.cファイル内のracewalk関数とcallinstr関数に集中しています。

  1. racewalk関数内の変更(racefuncenterの挿入とデバッグダンプ):

    • fn->enter = concat(list1(nd), fn->enter);
      • これは、racefuncenterの呼び出しを表すノードndを、関数のエントリ時に実行されるコードのリストfn->enter先頭に挿入する行です。list1(nd)ndのみを含む新しいリストを作成し、concatはそのリストと既存のfn->enterリストを結合します。これにより、関数内の他の初期化処理(例: new()呼び出し)よりも先にracefuncenterが実行されることが保証され、レース検出の範囲が関数の最初からカバーされるようになります。
    • snprint(s, sizeof(s), "enter %S", curfn->nname->sym);
    • dumplist(s, curfn->enter);
      • これらの行は、デバッグフラグdebug['W']が有効な場合に、現在の関数(curfn)のエントリコードリストcurfn->enterの内容をダンプするためのものです。これにより、コンパイラの開発者は、レース検出器が挿入したフックが正しく配置されているか、またfn->enterリストにどのようなコードが含まれているかを視覚的に確認できるようになります。
  2. racewalknode関数内の変更(OTYPESWのTODOコメント):

    • // TODO(dvyukov): the expression can contain calls or reads.
      • これは、OSWITCH(スイッチ文)のOTYPESW(型スイッチ)ケースにおけるコメントの変更です。以前のコメントは「静的型付けがあるため、気にする必要はない」というものでしたが、型スイッチの式自体が関数呼び出しやメモリ読み込みといった、レース検出器が監視すべき操作を含む可能性があるため、その点を考慮する必要があるというTODOコメントに置き換えられました。これは、将来的な改善の必要性を示しています。
  3. callinstr関数内の変更(ブランク識別子の厳密化):

    • if(strcmp(n->sym->name, "_") == 0)
      • この行は、n->sym->name(シンボル名)が厳密に_(ブランク識別子)であるかどうかをstrcmp関数で比較しています。以前はstrncmpが使用されており、_で始まるすべての名前を無視してしまう可能性がありましたが、strcmpを使用することで、正確にブランク識別子のみを無視するようになります。これは、レース検出器が不要なシンボルを追跡しないようにするための最適化であり、同時に_で始まる他の有効な識別子を誤って無視しないようにするための修正です。
    • 同様の修正が、構造体フィールドを走査するループ内のif(t1->sym && strcmp(t1->sym->name, "_"))にも適用されています。

これらの変更は、Goのレース検出器の内部動作をより正確かつ堅牢にし、デバッグを容易にすることを目的としています。

関連リンク

参考にした情報源リンク