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

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

このコミットは、Go言語のコンパイラ(cmd/gc)におけるレース検出器のインストゥルメンテーションに関するバグ修正です。具体的には、型スイッチ(type switch)が誤って過剰にインストゥルメントされる問題を解決しています。

コミット

commit a15074c4dc909c6e27a98f8464b79863f446e8cc
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Tue Jul 16 09:04:20 2013 +0200

    cmd/gc: fix race detector instrumentation of type switches.
    
    A type switch on a value with map index expressions,
    could get a spurious instrumentation from a OTYPESW node.
    These nodes do not need instrumentation because after
    walk the type switch has been turned into a sequence
    of ifs.
    
    Fixes #5890.
    
    R=golang-dev, dvyukov
    CC=golang-dev
    https://golang.org/cl/11308043

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

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

元コミット内容

cmd/gc: 型スイッチのレース検出器のインストゥルメンテーションを修正。

マップインデックス式を持つ値に対する型スイッチは、OTYPESWノードから誤ったインストゥルメンテーションを受ける可能性がありました。これらのノードは、ウォーク(AST変換)の後、型スイッチが一連のif文に変換されるため、インストゥルメンテーションを必要としません。

Issue #5890 を修正。

変更の背景

Go言語には、並行処理におけるデータ競合(data race)を検出するための「レース検出器(Race Detector)」が組み込まれています。これは、Goプログラムの実行時にメモリへのアクセスを監視し、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも一方が書き込み操作である場合に警告を発するツールです。レース検出器は、コンパイル時にコードに特別なインストゥルメンテーション(計測コード)を挿入することで機能します。

このコミット以前には、Goコンパイラ(cmd/gc)のレース検出器のインストゥルメンテーションロジックにバグが存在しました。具体的には、型スイッチ(type switch)の処理において、OTYPESWという内部的な抽象構文木(AST)ノードが、不必要にレース検出器のインストゥルメンテーションの対象となっていました。

問題は、型スイッチがコンパイルの過程で、最終的には一連のif文(またはif-else ifの連鎖)に変換されるという点にありました。この変換(「ウォーク」フェーズ)の後、元のOTYPESWノード自体は直接実行されるコードとしては存在せず、その配下の具体的な型アサーションや式が個別のif文として処理されます。したがって、OTYPESWノード自体にインストゥルメンテーションを施すことは冗長であり、場合によっては「スプリアス(spurious)」、つまり偽陽性のインストゥルメンテーションを引き起こす可能性がありました。コミットメッセージにある「マップインデックス式を持つ値に対する型スイッチ」という記述は、このような複雑な式が絡む場合に特に問題が顕在化しやすかったことを示唆しています。

この誤ったインストゥルメンテーションは、パフォーマンスのオーバーヘッドを増大させるだけでなく、レース検出器が誤った警告を発する可能性も示唆しています。Issue #5890 はこの問題が報告されたものであり、このコミットはその修正を目的としています。

前提知識の解説

  • Go言語のレース検出器 (Race Detector): Go 1.1から導入された、並行処理におけるデータ競合を検出するためのツールです。go run -racego build -racego test -raceなどのコマンドで有効にできます。コンパイル時にコードに計測コードを挿入し、実行時にメモリアクセスを監視することで、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも一方が書き込み操作である場合に警告します。これは、並行プログラムのデバッグにおいて非常に強力な機能です。

  • Goコンパイラ (cmd/gc): Go言語の公式コンパイラです。ソースコードを解析し、抽象構文木(AST)を構築し、最適化を行い、最終的に実行可能なバイナリを生成します。コンパイルの過程には、ASTの変換(「ウォーク」フェーズ)、型チェック、コード生成などが含まれます。

  • 抽象構文木 (AST - Abstract Syntax Tree): ソースコードの構造を木構造で表現したものです。コンパイラはソースコードを直接扱うのではなく、まずASTに変換してから様々な処理を行います。ASTの各ノードは、変数宣言、関数呼び出し、演算子、制御構造(if文、forループ、switch文など)といったプログラムの要素に対応します。

  • OTYPESWノード: Goコンパイラの内部で、型スイッチ(type switch)文を表すASTノードです。コンパイルの初期段階でソースコードの型スイッチに対応するノードとして生成されます。

  • ウォーク (Walk) フェーズ: Goコンパイラの重要なフェーズの一つで、ASTを走査(ウォーク)しながら、より低レベルの表現に変換したり、最適化を行ったりします。このフェーズで、高レベルな構文(例: type switch)が、より基本的な構文(例: if文の連鎖)に展開されることがあります。

  • インストゥルメンテーション (Instrumentation): プログラムの実行時の振る舞いを監視するために、追加のコードを挿入するプロセスです。レース検出器の場合、メモリアクセスが発生する箇所に、そのアクセスを記録・チェックするためのコードが挿入されます。

  • スプリアス (Spurious) インストゥルメンテーション: 不必要または誤ったインストゥルメンテーションのこと。この文脈では、レース検出器が監視する必要のない箇所に計測コードが挿入されてしまうことを指します。これにより、パフォーマンスの低下や、誤ったデータ競合の報告(偽陽性)につながる可能性があります。

技術的詳細

この修正は、Goコンパイラのsrc/cmd/gc/racewalk.cファイルに焦点を当てています。このファイルは、レース検出器のインストゥルメンテーションロジックを実装しており、ASTを走査しながら、メモリアクセスが発生する可能性のあるノードに計測コードを挿入するかどうかを決定します。

問題の核心は、racewalknode関数がASTノードを処理する際に、OTYPESWノードを特別に扱っていた点にありました。以前のコードでは、OTYPESWノードが検出されると、そのright子ノード(型スイッチの式に対応する部分)に対して再帰的にracewalknodeを呼び出していました。しかし、コミットメッセージが説明するように、OTYPESWノード自体は「ウォークの後、型スイッチが一連のif文に変換されるため、インストゥルメンテーションを必要としない」のです。つまり、OTYPESWノードはコンパイルの初期段階でのみ意味を持つ高レベルな概念であり、最終的な実行コードには直接対応しないため、このノード自体にレース検出器の計測コードを挿入することは無意味であり、誤りでした。

修正は、racewalknode関数からOTYPESWノードを処理するcase文を削除し、代わりにOTYPESWをインストゥルメンテーションをスキップすべきノードのリスト(case ONONAME: case OLITERAL: case OSLICESTR: ...)に追加することで行われました。これにより、OTYPESWノードはレース検出器のインストゥルメンテーションの対象から明示的に除外されるようになりました。

この変更により、型スイッチに関連する不要なインストゥルメンテーションが排除され、レース検出器の正確性と効率が向上します。特に、マップインデックス式のような複雑な式が型スイッチの対象となる場合に発生していた偽陽性のインストゥルメンテーションが解消されます。

テストケースとして追加されたTestRaceCaseTypeIssue5890は、この問題が実際に発生するシナリオを再現しています。マップの要素に対する型スイッチを使用し、異なるゴルーチンからマップの要素にアクセスすることで、修正前にはスプリアスなレース検出器の警告が発生していた状況をシミュレートしています。修正後、このテストは警告なしにパスするはずです。

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

src/cmd/gc/racewalk.c

--- a/src/cmd/gc/racewalk.c
+++ b/src/cmd/gc/racewalk.c
@@ -323,10 +323,6 @@ racewalknode(Node **np, NodeList **init, int wr, int skip)
 		racewalknode(&n->left, init, 0, 0);
 		goto ret;
 
-	case OTYPESW:
-		racewalknode(&n->right, init, 0, 0);
-		goto ret;
-
 	// should not appear in AST by now
 	case OSEND:
 	case ORECV:
@@ -402,6 +398,7 @@ racewalknode(Node **np, NodeList **init, int wr, int skip)
 	case ONONAME:
 	case OLITERAL:
 	case OSLICESTR:  // always preceded by bounds checking, avoid double instrumentation.
+	case OTYPESW:    // ignored by code generation, do not instrument.
 		goto ret;
 	}
 

src/pkg/runtime/race/testdata/mop_test.go

--- a/src/pkg/runtime/race/testdata/mop_test.go
+++ b/src/pkg/runtime/race/testdata/mop_test.go
@@ -262,6 +262,25 @@ func TestRaceCaseTypeBody(t *testing.T) {
 	<-c
 }
 
+func TestRaceCaseTypeIssue5890(t *testing.T) {
+	// spurious extra instrumentation of the initial interface
+	// value.
+	var x, y int
+	m := make(map[int]map[int]interface{})
+	m[0] = make(map[int]interface{})
+	c := make(chan int, 1)
+	go func() {
+		switch i := m[0][1].(type) {
+		case nil:
+		case *int:
+			*i = x
+		}
+		<-c
+	}()
+	m[0][1] = y
+	<-c
+}
+
 func TestNoRaceRange(t *testing.T) {
 	ch := make(chan int, 3)
 	a := [...]int{1, 2, 3}

コアとなるコードの解説

src/cmd/gc/racewalk.c の変更

  • 削除されたコード: racewalknode関数内のswitch文から、case OTYPESW:ブロックが削除されました。このブロックは、OTYPESWノードが検出された場合に、そのright子ノードに対して再帰的にracewalknodeを呼び出し、インストゥルメンテーションを試みていました。この削除により、OTYPESWノード自体に対する直接的なインストゥルメンテーション処理がなくなりました。

  • 追加されたコード: racewalknode関数内の別のswitch文(インストゥルメンテーションをスキップすべきノードを列挙している箇所)に、case OTYPESW:が追加されました。 case OTYPESW: // ignored by code generation, do not instrument. この行は、OTYPESWノードがコード生成によって無視されるため、インストゥルメントすべきではないことを明示的に示しています。これにより、OTYPESWノードは、ONONAME(名前のないノード)、OLITERAL(リテラル)、OSLICESTR(スライス文字列)などと同様に、レース検出器のインストゥルメンテーションの対象から除外されるようになりました。

この変更の論理は、OTYPESWノードがコンパイルの「ウォーク」フェーズでより基本的なif文のシーケンスに展開されるため、そのノード自体にインストゥルメンテーションを施す必要がないという理解に基づいています。実際のメモリアクセスは、展開されたif文の内部で行われるため、そちらで適切にインストゥルメントされれば十分です。

src/pkg/runtime/race/testdata/mop_test.go の変更

  • TestRaceCaseTypeIssue5890 関数の追加: この新しいテスト関数は、Issue #5890で報告された具体的なシナリオを再現するために追加されました。
    1. m := make(map[int]map[int]interface{})m[0] = make(map[int]interface{}) でネストされたマップを作成します。
    2. c := make(chan int, 1) でチャネルを作成し、ゴルーチン間の同期に使用します。
    3. 新しいゴルーチンを起動し、その中で switch i := m[0][1].(type) という型スイッチを実行します。この型スイッチは、マップの要素(m[0][1])に対して行われます。
    4. メインゴルーチンでは、m[0][1] = y という代入操作を行います。
    5. チャネルを使ってゴルーチン間の実行順序を制御し、データ競合が発生しうる状況を作り出します。

このテストの目的は、修正前にはm[0][1]に対する型スイッチの評価時に「スプリアスな追加インストゥルメンテーション」が発生し、レース検出器が誤った警告を発していたことを検証することです。修正後、このテストはクリーンにパスし、レース検出器が正しく動作することを確認します。

関連リンク

参考にした情報源リンク

  • コミットメッセージと差分(上記に記載)
  • Go言語の公式ドキュメント
  • Go言語のソースコード(src/cmd/gc/racewalk.c および src/pkg/runtime/race/testdata/mop_test.go
  • Go Race Detectorに関する公式ブログ記事
  • Goコンパイラの内部に関する一般的な知識
  • Issue #5890 (ただし、Web検索で得られた情報はGoLand IDEに関するものであり、このコミットが修正するGoランタイムのIssue #5890とは異なる可能性が高い。コミットメッセージに記載されたIssue番号は、Goの公式Issueトラッカーのものを指していると推測される。)
    • 注記: Web検索で得られた「Go issue 5890」はJetBrains GoLand IDEのフォーマットに関する問題であり、このコミットが修正するGoランタイムのIssue #5890とは直接関係がないようです。この解説は、主にコミットメッセージとコードの変更内容から推測されるGoランタイムのバグ修正として記述されています。