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

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

このコミットは、Goコンパイラのcmd/gcパッケージ内のracewalk機能のリファクタリングに関するものです。具体的には、レース検出器がコードを走査する方法を改善し、より汎用的なウォークメカニズムへの移行を目的としています。

コミット

commit 703043c8dcfdbf1d6b3fd5bea539da7392adce39
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Wed Nov 7 12:01:31 2012 +0400

    cmd/gc: refactor racewalk
    It is refactoring towards generic walk
    + it handles mode nodes.
    Partially fixes 4228 issue.
    
    R=golang-dev, lvd, rsc
    CC=golang-dev
    https://golang.org/cl/6775098

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

https://github.com/golang/go/commit/703043c8dcfdbf1d6b3fd5bea539da7392adce39

元コミット内容

cmd/gc: refactor racewalk It is refactoring towards generic walk + it handles mode nodes. Partially fixes 4228 issue.

変更の背景

このコミットの主な背景は、Goコンパイラのcmd/gcにおけるracewalk関数のリファクタリングです。racewalkはGoのレース検出器(Race Detector)の一部であり、プログラムのメモリアクセスを分析してデータ競合(data race)を検出する役割を担っています。

従来のracewalkの実装では、異なる種類のAST(Abstract Syntax Tree)ノード(例えば、OFOROIFOCALLFUNCなど)に対して、それぞれ個別の処理ロジックが記述されていました。これはコードの重複や複雑さを招き、新しいノードタイプが追加されるたびにracewalk関数も変更する必要がありました。

このコミットは、より「汎用的なウォーク(generic walk)」メカニズムへの移行を目指しています。これは、ASTの走査ロジックをより抽象化し、ノードの種類に依存しない共通の処理を導入することで、コードの保守性を高め、将来的な拡張を容易にすることを目的としています。また、コミットメッセージにある「it handles mode nodes」は、特定のノードタイプ(おそらく、以前は適切に処理されていなかったか、新しい処理が必要になったノード)のハンドリングが改善されたことを示唆しています。

さらに、「Partially fixes 4228 issue」とあるように、この変更は既存の課題(issue 4228)の部分的な修正にも貢献しています。ただし、Goの公開リポジトリで「issue 4228」という特定の課題を直接見つけることはできませんでした。これは、内部的な課題トラッカーの番号であるか、あるいは非常に古い課題である可能性があります。しかし、この記述から、racewalkの既存の動作に何らかの問題があり、今回のリファクタリングがその解決に寄与していることが推測されます。

前提知識の解説

このコミットを理解するためには、以下の概念に関する前提知識が必要です。

  1. Goコンパイラ (cmd/gc): Go言語の公式コンパイラであり、Goのソースコードを機械語に変換する役割を担っています。cmd/gcは、字句解析、構文解析、型チェック、最適化、コード生成など、コンパイルの様々な段階を実行します。
  2. 抽象構文木 (AST - Abstract Syntax Tree): ソースコードの抽象的な構文構造を木構造で表現したものです。コンパイラはソースコードをASTに変換し、このASTを走査しながら様々な分析や変換を行います。Goコンパイラでは、Node構造体がASTのノードを表します。
  3. レース検出器 (Race Detector): Go言語に組み込まれているツールで、並行プログラムにおけるデータ競合(data race)を検出します。データ競合は、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みであり、かつそれらのアクセスが同期メカニズムによって保護されていない場合に発生します。レース検出器は、プログラムの実行時にメモリアクセスを監視し、競合の可能性を報告します。
  4. racewalk関数: cmd/gc内のracewalk.cファイルに定義されている関数で、レース検出器がASTを走査し、メモリアクセスをインストゥルメント(計測コードを挿入)するために使用されます。この関数は、ASTの各ノードを訪問し、そのノードが表す操作(変数への読み書き、関数呼び出し、ループなど)に応じて適切な処理を行います。
  5. Node構造体: Goコンパイラ内部でASTの各ノードを表すC言語の構造体です。Nodeは、そのノードの操作タイプ(opフィールド、例: OFOR, OIF, OCALLFUNC)、左の子ノード(left)、右の子ノード(right)、初期化リスト(ninit)、ボディ(nbody)など、様々な情報を持ちます。
  6. NodeList: Nodeのリストを表す構造体で、例えばステートメントのブロックや関数の引数リストなどを表現するのに使われます。
  7. opnames.h: Goコンパイラ内部で使用されるヘッダーファイルで、ASTノードの操作タイプ(op)に対応する文字列名(例: OFOR -> "OFOR")を定義していました。デバッグ出力などでノードのタイプを人間が読める形式で表示するために使用されます。

技術的詳細

このコミットの技術的な詳細は、主にsrc/cmd/gc/racewalk.cファイルの変更に集約されます。

  1. opnames.hの削除: 変更前は#include "opnames.h"が含まれていましたが、変更後には削除されています。これは、racewalknode関数内のデバッグ出力でopnames[n->op]を使用していた部分が削除されたためです。この変更は、racewalkのコアロジックからデバッグ用の依存関係を切り離し、コードのクリーンアップに貢献しています。

  2. デバッグ出力の変更: 変更前のデバッグ出力は、print関数を使用してノードの操作タイプ、左右の子ノード、型、クラスなどを詳細に表示していました。

    // if(0) は常に偽なので、このコードは実行されないが、コメントアウトされている
    // print("op=%s, left=[ %N ], right=[ %N ], right's type=%T, n's type=%T, n's class=%d\n",
    // 	opnames[n->op], n->left, n->right, n->right ? n->right->type : nil, n->type, n->class);
    

    変更後には、debug['w'] > 1の場合にdump("racewalk-before", n)を呼び出すように変更されています。これは、より汎用的なデバッグメカニズムへの移行を示唆しており、特定のopnamesへの依存を排除しています。

  3. initリストのチェックの追加: racewalknode関数の冒頭に、init == nil || init == &n->ninitの場合にfatal("racewalk: bad init list")を呼び出すチェックが追加されました。これは、initリストが不正な状態で渡された場合に早期にエラーを検出するための堅牢性向上です。

  4. ノードタイプごとの処理の簡素化と汎用化: 最も重要な変更は、switch(n->op)ブロック内の多くのcase文から、racewalklistracewalknodeの明示的な呼び出しが削除されたことです。

    • OFOR, OIF, OCALLINTER, OCALLFUNC, OCALLMETH, ORETURN, OSELECT, OSWITCHなどの多くのノードタイプで、以前はそれぞれのcaseブロック内でracewalklistracewalknodeが呼び出されていました。
    • これらの呼び出しは削除され、代わりにgoto ret;switchブロックを抜けるようになっています。
  5. 汎用的なウォークロジックの導入: 削除された個別のウォーク呼び出しは、racewalknode関数の末尾にあるret:ラベルの後に集約されました。

    ret:
    	if(n->op != OBLOCK)  // OBLOCK is handled above in a special way.
    		racewalklist(n->list, init);
    	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);
    	racewalklist(n->nbody, nil);
    	racewalklist(n->nelse, nil);
    	racewalklist(n->rlist, nil);
    
    	*np = n;
    

    この新しいロジックは、ほとんどのノードタイプに対して共通の走査パターンを適用します。

    • n->list(例えば、関数呼び出しの引数リストや複合ステートメントのリスト)を走査します。
    • n->ntest(条件式、例: if文の条件)を走査し、その初期化リストを現在のノードのninitに結合します。
    • n->nincr(増分式、例: forループの増分部分)を走査し、その初期化リストを現在のノードのninitに結合します。
    • n->nbody(ブロック本体、例: if文やループの本体)を走査します。
    • n->nelseelseブロック)を走査します。
    • n->rlist(戻り値リスト)を走査します。

この変更により、racewalknodeは各ノードタイプ固有の処理を最小限に抑え、共通の走査ロジックを適用することで、より「汎用的なウォーク」を実現しています。これにより、新しいノードタイプが追加された場合でも、racewalknodeの変更が不要になるか、最小限で済むようになります。

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

src/cmd/gc/racewalk.cファイルにおいて、以下の変更が行われました。

  1. opnames.hの削除:

    --- a/src/cmd/gc/racewalk.c
    +++ b/src/cmd/gc/racewalk.c
    @@ -15,7 +15,6 @@
     #include <u.h>
     #include <libc.h>
     #include "go.h"
    -#include "opnames.h"
    
     // TODO(dvyukov): do not instrument initialization as writes:
     // a := make([]int, 10)
    
  2. デバッグ出力とinitリストチェックの変更:

    --- a/src/cmd/gc/racewalk.c
    +++ b/src/cmd/gc/racewalk.c
    @@ -88,16 +87,19 @@ static void
     racewalknode(Node **np, NodeList **init, int wr, int skip)
     {
     	Node *n, *n1;
    +	NodeList *l;
     	NodeList *fini;
    
     	n = *np;
    
     	if(n == N)
     		return;
    -	if(0)
    -		print("op=%s, left=[ %N ], right=[ %N ], right's type=%T, n's type=%T, n's class=%d\n",
    -			opnames[n->op], n->left, n->right, n->right ? n->right->type : nil, n->type, n->class);
    +
    +	if(debug['w'] > 1)
    +		dump("racewalk-before", n);
     	setlineno(n);
    +	if(init == nil || init == &n->ninit)
    +		fatal("racewalk: bad init list");
    
     	racewalklist(n->ninit, nil);
    
  3. ノードタイプごとのウォーク呼び出しの削除: OFOR, OIF, OCALLINTER, OCALLFUNC, OCALLMETH, ORETURN, OSELECT, OSWITCHなどのcaseブロックから、racewalklistracewalknodeの呼び出しが削除され、goto ret;が追加されました。

    例: OFOR

    --- a/src/cmd/gc/racewalk.c
    +++ b/src/cmd/gc/racewalk.c
    @@ -146,16 +148,9 @@ racewalknode(Node **np, NodeList **init, int wr, int skip)
     	goto ret;
    
     case OFOR:
    -	if(n->ntest != N)
    -		racewalklist(n->ntest->ninit, nil);
    -	racewalknode(&n->nincr, init, wr, 0);
    -	racewalklist(n->nbody, nil);
     	goto ret;
    
  4. 汎用的なウォークロジックの追加: ret:ラベルの後に、共通のウォークロジックが追加されました。

    --- a/src/cmd/gc/racewalk.c
    +++ b/src/cmd/gc/racewalk.c
    @@ -375,6 +360,18 @@ racewalknode(Node **np, NodeList **init, int wr, int skip)
     	}
    
     ret:
    +	if(n->op != OBLOCK)  // OBLOCK is handled above in a special way.
    +		racewalklist(n->list, init);
    +	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);
    +	racewalklist(n->nbody, nil);
    +	racewalklist(n->nelse, nil);
    +	racewalklist(n->rlist, nil);
    +
     	*np = n;
     }
    

コアとなるコードの解説

このコミットのコアとなるコードの変更は、racewalknode関数におけるASTノードの走査方法の根本的な変更です。

変更前は、switch(n->op)文の中で、各ノードタイプ(OFOR, OIF, OCALLFUNCなど)に対して、そのノードが持つ子ノードやリスト(n->ntest, n->nincr, n->nbody, n->listなど)を明示的にracewalknoderacewalklistで走査していました。これは、ノードタイプごとに走査ロジックが散在し、冗長性や保守性の問題を引き起こしていました。

変更後では、これらの個別の走査呼び出しがswitch文の中から削除され、代わりにgoto ret;によって関数の末尾にある共通の処理ブロックに制御が移されます。この共通の処理ブロックでは、ほとんどのノードが持つ可能性のある一般的な子ノードやリスト(n->list, n->ntest, n->nincr, n->nbody, n->nelse, n->rlist)をまとめて走査します。

このアプローチの利点は以下の通りです。

  • 汎用性: ほとんどのASTノードは、共通の構造(例えば、条件、増分、ボディ、リストなど)を持っています。この変更により、これらの共通部分を一度に処理する汎用的なウォークロジックが確立されます。
  • コードの簡素化と重複の排除: 各caseブロックから重複する走査ロジックが削除され、コードが大幅に簡素化されました。
  • 保守性の向上: 新しいASTノードタイプが追加された場合でも、そのノードが一般的な構造を持つ限り、racewalknode関数内のswitch文に新しいcaseを追加する必要がなくなります。これにより、将来的な拡張が容易になります。
  • 「mode nodes」のハンドリング: コミットメッセージにある「it handles mode nodes」は、この汎用的なウォークロジックが、以前は適切に処理されていなかったか、特別な考慮が必要だったノードタイプも適切に走査できるようになったことを示唆しています。

ただし、OBLOCKノードは特殊な方法で処理されるため、汎用的なウォークロジックの対象外とされています。これは、OBLOCKが他のノードとは異なる構造や走査要件を持つためと考えられます。

全体として、この変更はracewalkの内部構造をよりクリーンで、拡張性が高く、保守しやすいものにするための重要なリファクタリングです。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード (src/cmd/gc/racewalk.c)
  • Go言語のレース検出器に関する一般的な情報
  • GoコンパイラのAST構造に関する一般的な情報
  • Gitコミットメッセージの解析
  • Web検索: "golang issue 4228" (直接的な情報は見つからず、内部的な課題番号である可能性が高いと判断)