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

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

このコミットは、Go言語のコンパイラ gc にデータ競合検出機能の初期部分を導入するものです。具体的には、コンパイラに -b フラグが与えられた際に、生成されるバイナリコードにメモリアクセス(読み書き)および関数エントリー/エグジットの計測コードを挿入する機能を追加します。これは、Goのデータ競合検出ツール(Race Detector)の基盤となる重要な変更であり、racewalk.c という新しいファイルが追加され、既存のコンパイラ関連ファイルが修正されています。

コミット

race: gc changes
This is the first part of a bigger change that adds data race detection feature:
https://golang.org/cl/6456044
This change makes gc compiler instrument memory accesses when supplied with -b flag.

R=rsc, nigeltao, lvd
CC=golang-dev
https://golang.org/cl/6497074

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

https://github.com/golang/go/commit/041fc8bf96993d7405d938c7f4ad0b6ec474a91a

元コミット内容

commit 041fc8bf96993d7405d938c7f4ad0b6ec474a91a
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Tue Oct 2 10:05:46 2012 +0400

    race: gc changes
    This is the first part of a bigger change that adds data race detection feature:
    https://golang.org/cl/6456044
    This change makes gc compiler instrument memory accesses when supplied with -b flag.
    
    R=rsc, nigeltao, lvd
    CC=golang-dev
    https://golang.org/cl/6497074

変更の背景

並行プログラミングにおいて、複数のゴルーチンが共有メモリに同時にアクセスし、少なくとも一方が書き込み操作である場合に発生する「データ競合(Data Race)」は、プログラムの予測不能な動作やバグの主要な原因となります。Go言語は並行処理を強力にサポートしていますが、データ競合は依然として開発者が直面する課題でした。

このコミットは、Go言語に組み込みのデータ競合検出機能(Go Race Detector)を導入するための第一歩です。開発者がコンパイル時に特定のフラグ(-b)を指定することで、実行時にデータ競合を検出できるようになることを目指しています。これにより、並行プログラムのデバッグと信頼性向上が大いに促進されます。コミットメッセージに記載されている通り、これはより大きな変更(https://golang.org/cl/6456044)の一部であり、コンパイラレベルでの計測(instrumentation)がその基盤となります。

前提知識の解説

データ競合 (Data Race)

データ競合とは、以下の3つの条件がすべて満たされたときに発生する並行処理のバグです。

  1. 少なくとも2つのゴルーチンが同じメモリ位置にアクセスする。
  2. 少なくとも1つのアクセスが書き込みである。
  3. これらのアクセスが同期メカニズム(ミューテックス、チャネルなど)によって保護されていない。

データ競合が発生すると、プログラムの動作は未定義となり、結果が予測不能になったり、クラッシュしたりする可能性があります。Go言語では、sync パッケージの MutexRWMutex、あるいはチャネルを用いた通信によって共有メモリへのアクセスを同期させることが推奨されています。

コンパイラによる計測 (Instrumentation)

計測とは、プログラムの実行時の振る舞いを監視するために、コンパイラやリンカが自動的に追加のコードを挿入する技術です。データ競合検出においては、メモリの読み書きや関数の呼び出し/終了といったイベントが発生するたびに、特別なランタイム関数を呼び出すコードを挿入します。これらのランタイム関数は、メモリアクセスの履歴を追跡し、競合パターンを検出します。

Goコンパイラ (gc)

gc はGo言語の公式コンパイラであり、src/cmd/gc ディレクトリにそのソースコードがあります。Goのソースコードを中間表現に変換し、最終的に機械語コードを生成する役割を担っています。このコミットでは、gc がAST(抽象構文木)を走査し、計測コードを挿入する新しいパス(racewalk)を追加しています。

uintptr

uintptr はGo言語の組み込み型で、ポインタを整数として表現できる型です。ポインタ演算を行う際に使用されることがありますが、ガベージコレクタの対象外であるため、注意して使用する必要があります。データ競合検出においては、メモリアドレスをランタイム関数に渡す際に uintptr に変換して使用されます。

技術的詳細

このコミットの主要な技術的変更点は、Goコンパイラ gc にデータ競合検出のためのコード計測機能を追加したことです。

  1. -b フラグの導入:

    • src/cmd/gc/doc.go に、gc コンパイラに -b フラグを追加したことが明記されました。このフラグを付けてコンパイルすると、データ競合検出が有効になります。
    • src/cmd/gc/lex.c では、-b フラグが指定された場合に runtime/race パッケージをインポートし、パッケージパスに _race サフィックスを追加するロジックが追加されました。これは、競合検出用に特別にコンパイルされたパッケージ(例えば、sync_race など)をロードできるようにするためと考えられます。
  2. racewalk パスの追加:

    • src/cmd/gc/pgen.ccompile 関数内で、-b フラグが有効な場合に racewalk(curfn) が呼び出されるようになりました。これは、racewalk がコンパイルプロセスの後半で、各関数のASTに対して計測処理を行うことを意味します。
    • src/cmd/gc/racewalk.c の新規追加: このファイルがコミットの核心です。
      • racewalk 関数は、コンパイル対象の関数(fn)のASTを走査し、計測コードを挿入します。
      • 関数のエントリー時に racefuncenter を、エグジット時に racefuncexit を呼び出すコードを挿入します。
      • racewalknode および callinstr 関数は、ASTノードを再帰的に走査し、メモリの読み書き操作(OAS, ODOT, OIND, OINDEX, ONAME など)を検出します。
      • 検出されたメモリ操作の前に、raceread または racewrite を呼び出すコードを挿入します。これらの関数には、アクセスされるメモリのアドレスが uintptr 型で渡されます。
      • uintptraddr ヘルパー関数は、与えられたノードのアドレスを取得し、uintptr 型に変換します。
      • basenod ヘルパー関数は、ODOTOINDEX のような複合的なアクセスの場合に、基底となるノード(例えば、構造体のフィールドアクセスにおける構造体変数自体)を特定します。
      • 計測の除外: omitPkgs 配列("runtime", "runtime/race", "sync", "sync/atomic")にリストされているパッケージは、計測の対象外とされます。これは、これらのパッケージ自体が競合検出のランタイムを構成するため、あるいは低レベルの同期プリミティブを提供するため、計測すると無限ループや不正確な結果、過剰なオーバーヘッドを引き起こす可能性があるためです。
  3. ランタイム関数の宣言:

    • src/cmd/gc/builtin.csrc/cmd/gc/runtime.go に、racefuncenter, racefuncexit, raceread, racewrite の関数シグネチャが追加されました。これらはGoのランタイム(おそらくCやアセンブリで実装される)によって提供される関数であり、コンパイラが生成するコードから呼び出されます。
    • src/cmd/gc/go.h には、runtime/race パッケージを表す racepkg の宣言が追加されました。
    • src/cmd/gc/reflect.c では、-b フラグが有効な場合に racepkg のインポートパスが追加されるようになりました。

これらの変更により、Goコンパイラは、データ競合検出に必要なフックをプログラムの実行パスに挿入できるようになり、Go Race Detectorの実現に向けた重要な一歩となりました。

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

このコミットのコアとなる変更は、主に以下のファイルと関数に集中しています。

  • src/cmd/gc/racewalk.c (新規ファイル):

    • racewalk(Node *fn): 各関数のASTを走査し、計測コードを挿入するメイン関数。
    • racewalklist(NodeList *l, NodeList **init): ノードリストを走査するヘルパー関数。
    • racewalknode(Node **np, NodeList **init, int wr, int skip): 個々のASTノードを走査し、メモリアクセスを特定する再帰関数。
    • callinstr(Node *n, NodeList **init, int wr, int skip): 実際に計測コード(raceread または racewrite の呼び出し)を挿入する関数。
    • uintptraddr(Node *n): ノードのアドレスを uintptr に変換する関数。
    • basenod(Node *n): 複合的なアクセス(例: a.b.carr[i])の基底ノードを特定する関数。
  • src/cmd/gc/pgen.c:

    • compile(Node *fn): この関数内で、-b フラグが有効な場合に racewalk(curfn) が呼び出されます。
  • src/cmd/gc/builtin.c および src/cmd/gc/runtime.go:

    • racefuncenter(), racefuncexit(), raceread(uintptr), racewrite(uintptr) の関数シグネチャが追加されました。これらはコンパイラが生成する計測コードから呼び出されるランタイム関数です。

コアとなるコードの解説

src/cmd/gc/racewalk.c

このファイルは、データ競合検出のためのコード計測(instrumentation)ロジックを実装しています。

  • racewalk(Node *fn):

    • この関数は、コンパイル中の関数 fn のASTを受け取ります。
    • まず、omitPkgs リストに含まれるパッケージ(runtime, runtime/race, sync, sync/atomic)の場合、計測をスキップします。これは、これらのパッケージが競合検出のランタイム自体を構成したり、低レベルの同期プリミティブを提供したりするため、計測すると不正確な結果や無限ループを引き起こす可能性があるためです。
    • 関数の開始時に racefuncenter() を、終了時に racefuncexit() を呼び出すコードをASTに挿入します。これにより、関数の実行範囲を追跡できます。
    • racewalklist(curfn->nbody, nil) を呼び出し、関数の本体(nbody)のASTを走査して、個々のステートメントや式に計測コードを挿入します。
  • racewalknode(Node **np, NodeList **init, int wr, int skip):

    • この関数は、ASTノード n を再帰的に走査します。
    • wr パラメータは、現在の操作が書き込み(1)か読み込み(0)かを示します。
    • skip パラメータは、計測をスキップすべきかどうかを示します。
    • switch(n->op) 文で様々なASTノードのタイプを処理します。
      • OASOP, OAS, OAS2 などの代入操作では、左辺(書き込み)と右辺(読み込み)に対して再帰的に racewalknode を呼び出します。
      • ODOT, ODOTPTR, OIND, OINDEX, ONAME などのメモリアクセスを表すノードに対しては、callinstr を呼び出して計測コードを挿入します。
      • OFOR, OIF, OSWITCH などの制御フローノードでは、条件式やボディ、初期化リストなどを再帰的に走査します。
      • OCALLFUNC, OCALLINTER, OCALLMETH などの関数呼び出しでは、引数リストなどを走査します。
      • OLEN, OCAP など、一部の組み込み関数は、その引数に対して計測を行います。
      • OADDR (アドレス取得) の場合は、その対象自体へのアクセスではないため、skip フラグを立てて racewalknode を再帰呼び出しします。
      • OINDEXMAP, OPRINT など、一部の操作は計測をスキップします。
      • OCMPSTR, OAPPEND など、まだ実装されていない(unimplemented)操作もスキップされます。
  • callinstr(Node *n, NodeList **init, int wr, int skip):

    • この関数は、実際に raceread または racewrite の呼び出しを挿入するかどうかを決定します。
    • skip が真の場合や、ノードの型が不明な場合、あるいは理想型(TIDEAL)の場合は計測をスキップします。
    • ONAME ノードの場合、アンダースコアで始まる変数名(_autotmp_, statictmp_)は計測をスキップします。これらはコンパイラが生成する一時変数や未使用変数であり、通常は競合の対象とならないためです。
    • 構造体(TSTRUCT)の場合、そのフィールドを再帰的に走査し、各フィールドへのアクセスを計測します。
    • basenod(n) を呼び出して、アクセス対象の基底ノードを取得します。
    • 基底ノードのクラス(class)が PHEAP(ヒープ上の変数)、PPARAMREF(参照渡しされたパラメータ)、PEXTERN(外部変数)、または配列(TARRAY)、ポインタ経由のアクセス(ODOTPTR, OIND, OXDOT)である場合にのみ、計測コードを挿入します。これは、スタック上のローカル変数へのアクセスは通常、単一ゴルーチン内でのみ行われるため、競合の対象とならないという前提に基づいています。
    • mkcall を使用して、wr の値に応じて racewrite または raceread の呼び出しノードを作成します。
    • uintptraddr(n) を呼び出して、アクセスされるメモリのアドレスを uintptr 型で取得し、ランタイム関数に引数として渡します。
    • 生成された呼び出しノードは、現在のノードの初期化リスト(init)に追加されます。

src/cmd/gc/pgen.c

  • compile 関数は、GoのソースコードからASTを生成し、様々な最適化や変換パスを適用するコンパイラの主要な部分です。
  • このコミットでは、racewalk(curfn) の呼び出しが追加され、-b フラグが有効な場合にデータ競合検出の計測パスが実行されるようになりました。これにより、コンパイルされたプログラムに競合検出のためのフックが組み込まれます。

src/cmd/gc/builtin.c および src/cmd/gc/runtime.go

これらのファイルは、コンパイラが生成するコードから呼び出されるランタイム関数の宣言を提供します。

  • builtin.c は、Goの組み込み関数やランタイム関数をコンパイラが認識するためのC言語の定義を含んでいます。ここに racefuncenter, racefuncexit, raceread, racewrite の宣言が追加されたことで、コンパイラはこれらの関数がGoのランタイムに存在することを認識し、適切に呼び出しコードを生成できるようになります。
  • runtime.go は、Goのランタイムパッケージの一部であり、Go言語で書かれたランタイム関数の宣言を含んでいます。ここにこれらの関数が宣言されたことで、Goのコードからこれらの関数を呼び出すことが可能になります(ただし、実際のGo Race Detectorのランタイム実装は、より低レベルのCやアセンブリで書かれることが多いです)。

これらの変更は、Goコンパイラがデータ競合検出機能をサポートするための基盤を構築し、Goプログラムの並行処理のデバッグと信頼性向上に大きく貢献します。

関連リンク

参考にした情報源リンク