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

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

このコミットは、Go言語のコンパイラ(cmd/gc)におけるレース検出器の計装(instrumentation)に関する修正です。具体的には、BLOCKノードの処理方法を改善し、関数呼び出しやインライン化された関数に関連するブロックが適切にレース検出の対象となるように変更しています。

コミット

commit c46f1f40daad83c940e1b3c09f77b9867d598473
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Sat Nov 3 00:11:06 2012 +0100

    cmd/gc: instrument blocks for race detection.
    
    It happens that blocks are used for function calls in a
    quite low-level way so they cannot be instrumented as
    usual.
    
    Blocks are also used for inlined functions.
    
    R=golang-dev, rsc, dvyukov
    CC=golang-dev
    https://golang.org/cl/6821068

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

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

元コミット内容

cmd/gc: instrument blocks for race detection.

このコミットは、Goコンパイラ(cmd/gc)において、レース検出のためにブロック(BLOCKノード)を計装するものです。 コミットメッセージによると、ブロックは関数呼び出しのために低レベルで使用されるため、通常の方法では計装できない問題がありました。また、インライン化された関数にもブロックが使用されることが指摘されています。

変更の背景

Go言語には、並行処理におけるデータ競合(data race)を検出するための組み込みのレース検出器(Race Detector)があります。データ競合は、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みであり、かつそれらのアクセスが同期メカニズムによって保護されていない場合に発生するバグです。これはプログラムの予測不能な動作やクラッシュを引き起こす可能性があります。

レース検出器は、コンパイル時または実行時にコードに特殊な計装(instrumentation)を挿入することで機能します。この計装は、メモリへのアクセス(読み書き)を監視し、競合のパターンを検出します。

このコミットの背景には、Goコンパイラ(cmd/gc)が生成する中間表現(IR)において、BLOCKノードが特定の状況(特に複数の戻り値を持つ関数呼び出しやインライン化された関数)で特殊な役割を果たしており、これらのブロックがレース検出器によって適切に計装されていなかったという問題がありました。

従来の処理では、OBLOCKノードに遭遇すると「leads to crashes. //racewalklist(n->list, nil);」というコメントと共に処理がスキップされており、これがレース検出の漏れにつながっていたと考えられます。このコミットは、この問題を解決し、より網羅的なレース検出を可能にすることを目的としています。

前提知識の解説

Go言語のコンパイラ (cmd/gc)

cmd/gcは、Go言語の公式コンパイラです。Goのソースコードを機械語に変換する役割を担っています。コンパイルプロセスには、字句解析、構文解析、型チェック、中間コード生成、最適化、コード生成など、複数のフェーズがあります。このコミットが関連するracewalk.cは、中間コードの走査とレース検出器のための計装を行う部分です。

Go言語のレース検出器 (Race Detector)

Goのレース検出器は、実行時にデータ競合を検出するツールです。go run -racego build -racego test -raceなどのコマンドで有効にできます。これは、プログラムの実行中にメモリへのアクセスを監視し、競合が発生した可能性のある場所を報告します。内部的には、ThreadSanitizer (TSan) の技術をベースにしています。TSanは、メモリへのアクセス履歴を記録し、並行アクセスパターンを分析することでデータ競合を検出します。

抽象構文木 (AST) と中間表現 (IR)

コンパイラは、ソースコードを直接機械語に変換するのではなく、いくつかの抽象的な表現に変換しながら処理を進めます。

  • 抽象構文木 (AST): ソースコードの構文構造を木構造で表現したものです。各ノードは、式、文、宣言などを表します。
  • 中間表現 (IR): ASTよりも低レベルで、機械語に近いが、特定のCPUアーキテクチャに依存しない形式です。コンパイラの最適化フェーズで利用されます。Goコンパイラでは、ASTからIRへの変換が行われ、このIRに対して様々な最適化や計装が施されます。

BLOCKノード (OBLOCK)

Goコンパイラの中間表現において、BLOCKノード(OBLOCK)は、複数のステートメントをまとめるために使用される構造です。これは、C言語の{ ... }ブロックや、複数の文を順次実行するシーケンスに似ています。 コミットメッセージが示唆するように、BLOCKノードは単なるコードブロックだけでなく、以下のような低レベルな用途にも使われます。

  1. 複数の戻り値を持つ関数呼び出し: Goでは、関数が複数の値を返すことができます。例えば、x, y := f()のような呼び出しは、コンパイラ内部ではBLOCKノードとして表現されることがあります。このブロック内には、関数呼び出し自体と、戻り値をそれぞれの変数に割り当てる操作が含まれます。
  2. インライン化された関数: コンパイラ最適化の一環として、小さな関数は呼び出し元に直接展開(インライン化)されることがあります。インライン化された関数の本体も、BLOCKノードとして表現されることがあります。
  3. ループの初期化: forループなどの初期化部分もBLOCKノードで表現されることがあります。

racewalknoderacewalklist

racewalknoderacewalklistは、src/cmd/gc/racewalk.cファイル内で定義されている関数で、Goコンパイラのレース検出器の計装フェーズにおいて、AST/IRノードを走査(walk)し、必要な計装コードを挿入する役割を担っています。

  • racewalknode: 単一のノードを走査します。
  • racewalklist: ノードのリストを走査します。

技術的詳細

このコミットの核心は、racewalk.c内のracewalknode関数におけるOBLOCKノードの処理ロジックの変更です。

変更前は、OBLOCKノードに遭遇すると、その内部のリスト(n->list)の走査がコメントアウトされており、クラッシュを引き起こす可能性があるためスキップされていました。これは、OBLOCKが特定の低レベルな構造(特に複数の戻り値を持つ関数呼び出し)で使用される際に、単純な走査と計装が問題を引き起こすことを示唆しています。例えば、x, y := f()のようなコードがBLOCK{CALL f, AS x [SP+0], AS y [SP+n]}のように表現される場合、CALL fAS xAS yの間にレース検出の計装を挿入すると、関数の戻り値がスタックに配置される前に計装コードが実行され、結果が破壊される("smash the results")可能性がありました。

このコミットでは、OBLOCKノードの処理をより詳細に制御するために、n->list->n->op(ブロック内の最初のステートメントの操作)に基づいてswitch文を導入しています。

  1. 関数呼び出しのブロック (OCALLFUNC, OCALLMETH, OCALLINTER):

    • これらのケースでは、ブロックが複数の戻り値を持つ関数呼び出しのために使用されていると判断されます。
    • racewalknode(&n->list->n, &n->ninit, 0, 0); を呼び出すことで、関数呼び出し自体(n->list->n)のみを計装します。この際、wr(書き込みフラグ)は0(読み込み)に設定され、skip0です。
    • fini = nil; racewalklist(n->list->next, &fini); を呼び出すことで、**関数呼び出しに続くステートメント(戻り値の割り当てなど)**を計装します。この際、finiリストに計装結果が格納されます。
    • n->list = concat(n->list, fini); によって、元のブロックリストと、後続のステートメントの計装結果を結合します。
    • このアプローチにより、関数呼び出しと戻り値の割り当ての間に計装コードが挿入されることを避け、結果が破壊されるのを防ぎつつ、関数呼び出し自体はレース検出の対象とすることができます。
  2. その他の通常のブロック (default):

    • 上記以外のOBLOCKノード(例: ループの初期化、インライン化された関数の本体など)は、通常のブロックとして扱われます。
    • racewalklist(n->list, nil); を呼び出すことで、ブロック内のすべてのステートメントを通常通り再帰的に走査し、計装を適用します。これにより、インライン化された関数などの内部も適切にレース検出の対象となります。

この変更により、Goコンパイラは、これまでスキップされていた特定のBLOCKノードに対してもレース検出の計装を適用できるようになり、レース検出器の網羅性と精度が向上します。

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

変更はsrc/cmd/gc/racewalk.cファイル内のracewalknode関数に集中しています。

--- a/src/cmd/gc/racewalk.c
+++ b/src/cmd/gc/racewalk.c
@@ -88,6 +88,7 @@ static void
 racewalknode(Node **np, NodeList **init, int wr, int skip)\n {\n \tNode *n, *n1;\n+\tNodeList *fini;\n \n \tn = *np;\n \n@@ -116,8 +117,28 @@ racewalknode(Node **np, NodeList **init, int wr, int skip)\n \t\tgoto ret;\n \n \tcase OBLOCK:\n-\t\t// leads to crashes.\n-\t\t//racewalklist(n->list, nil);\n+\t\tif(n->list == nil)\n+\t\t\tgoto ret;\n+\n+\t\tswitch(n->list->n->op) {\n+\t\tcase OCALLFUNC:\n+\t\tcase OCALLMETH:\n+\t\tcase OCALLINTER:\n+\t\t\t// Blocks are used for multiple return function calls.\n+\t\t\t// x, y := f() becomes BLOCK{CALL f, AS x [SP+0], AS y [SP+n]}\n+\t\t\t// We don\'t want to instrument between the statements because it will\n+\t\t\t// smash the results.\n+\t\t\tracewalknode(&n->list->n, &n->ninit, 0, 0);\n+\t\t\tfini = nil;\n+\t\t\tracewalklist(n->list->next, &fini);\n+\t\t\tn->list = concat(n->list, fini);\n+\t\t\tbreak;\n+\n+\t\tdefault:\n+\t\t\t// Ordinary block, for loop initialization or inlined bodies.\n+\t\t\tracewalklist(n->list, nil);\n+\t\t\tbreak;\n+\t\t}\n \t\tgoto ret;\n \n \tcase ODEFER:\

コアとなるコードの解説

変更のポイントは、case OBLOCK: の部分です。

  1. NodeList *fini; の追加:

    • racewalknode関数のローカル変数としてfiniというNodeListポインタが追加されました。これは、関数呼び出しのブロックを処理する際に、後続のステートメントの計装結果を一時的に保持するために使用されます。
  2. OBLOCK処理の変更:

    • 変更前は、OBLOCKケースは単にコメントアウトされたracewalklist(n->list, nil);を含んでおり、実質的に何も処理していませんでした。

    • 変更後は、まずif(n->list == nil) goto ret;で空のブロックをスキップします。

    • 次に、switch(n->list->n->op)文が導入され、ブロック内の最初のノードの操作(op)に基づいて処理を分岐します。

    • case OCALLFUNC: case OCALLMETH: case OCALLINTER::

      • これらのケースは、通常の関数呼び出し、メソッド呼び出し、インターフェースメソッド呼び出しを表します。
      • racewalknode(&n->list->n, &n->ninit, 0, 0);
        • これは、ブロックの最初の要素である関数呼び出しノード自体を走査し、計装します。n->ninitは初期化リスト、0, 0はそれぞれ書き込みフラグとスキップフラグです。
      • fini = nil; racewalklist(n->list->next, &fini);
        • 関数呼び出しに続く、ブロック内の残りのステートメント(例: 複数の戻り値の変数への割り当て)を走査し、計装します。計装されたノードはfiniリストに追加されます。
      • n->list = concat(n->list, fini);
        • 元のブロックリスト(関数呼び出しノードを含む)と、後続のステートメントの計装結果を含むfiniリストを結合し、新しいブロックリストとしてn->listに設定します。これにより、関数呼び出しと戻り値の割り当ての間に計装コードが挿入されることを防ぎつつ、両方が適切に処理されます。
    • default::

      • 上記以外のOBLOCKノード(例: ループの初期化、インライン化された関数の本体など)は、通常のブロックとして扱われます。
      • racewalklist(n->list, nil);
        • ブロック内のすべてのステートメントを再帰的に走査し、計装を適用します。これは、変更前のコメントアウトされていた行が、特定の条件で有効になった形です。

この変更により、Goのレース検出器は、これまで見逃されていた可能性のあるデータ競合を、より正確に検出できるようになります。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード(特にsrc/cmd/gc/racewalk.c
  • ThreadSanitizerに関する一般的な情報
  • データ競合と並行プログラミングに関する一般的な知識
  • コミットメッセージと関連するGoのコードレビュー(CL: Change List): https://golang.org/cl/6821068 (これはコミットメッセージに記載されているリンクであり、詳細な議論が含まれている可能性があります。)
  • Goコンパイラの内部構造に関するブログ記事や解説(一般的な知識として)