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

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

このコミットは、Goコンパイラ(cmd/gc)におけるデータ競合検出器のインストゥルメンテーションに関するバグ修正です。具体的には、T(v).Fieldのようなセレクタ(型変換後のフィールドアクセス)に対して、競合検出器が正しく動作しない問題を解決します。この修正により、Goのデータ競合検出器がより堅牢になり、特定のコードパターンにおける競合状態を見逃すことがなくなります。

コミット

commit 6c4943cb51cc7c4b27233a7717d74742871f7faa
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Wed May 15 01:25:20 2013 +0200

    cmd/gc: fix race instrumentation of selectors T(v).Field

    Fixes #5424.

    R=golang-dev, daniel.morsing, dvyukov, r
    CC=golang-dev
    https://golang.org/cl/9033048

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

https://github.com/golang/go/commit/6c4943cb51cc7c4b27233a7717d74742871f7faa

元コミット内容

このコミットは、Goコンパイラのcmd/gc部分におけるデータ競合検出器のインストゥルメンテーションに関する修正です。特に、T(v).Fieldという形式のセレクタ(型変換後のフィールドアクセス)に対して、競合検出器が正しく動作しない問題を修正します。これは、GoのIssue #5424で報告されたバグに対応するものです。

変更の背景

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

このコミット以前は、T(v).Fieldのような特定のコードパターン、すなわち型変換(T(v))の後にその結果の構造体のフィールドにアクセスする(.Field)場合、競合検出器がそのメモリアクセスを正しくインストゥルメント(計測)できていませんでした。インストゥルメンテーションとは、コンパイラが実行時にメモリアクセスを監視するための追加コードを挿入するプロセスを指します。正しくインストゥルメントされない場合、実際にデータ競合が発生していても検出器がそれを報告しないという問題が生じます。

Issue #5424では、この問題が具体的に報告されており、T(v).Fieldのような式が、競合検出器にとって「アドレス可能(addressable)」ではないと誤って判断されることが原因でした。Goの競合検出器は、メモリ位置のアドレスを取得してそのアクセスを記録するため、アドレス可能でない式に対してはインストゥルメンテーションを行うことができません。このコミットは、この特定のエッジケースを修正し、競合検出器の信頼性を向上させることを目的としています。

前提知識の解説

Go言語のデータ競合検出器 (Race Detector)

Goのデータ競合検出器は、Go 1.1で導入された強力なツールです。go run -racego build -racego test -raceなどのコマンドで有効にできます。有効にすると、コンパイラはプログラムのメモリアクセスを監視するための追加コードを挿入します。実行時に、この監視コードが並行アクセスを検出し、データ競合の可能性を報告します。これは、スレッドサニタイザー(ThreadSanitizer, TSan)という技術に基づいています。

cmd/gc

cmd/gcは、Go言語の公式コンパイラのバックエンド部分を指します。Goのソースコードを機械語に変換する主要な役割を担っています。競合検出器のインストゥルメンテーションは、このコンパイラのフェーズで行われます。具体的には、racewalk.cのようなファイルが、メモリアクセスを検出して競合検出器のランタイム関数呼び出しを挿入する役割を担っています。

セレクタ (Selectors)

Go言語において、セレクタは構造体のフィールドやメソッドにアクセスするための構文です。例えば、s.Fields.Method()のように使用されます。このコミットで問題となっているのは、T(v).Fieldのような形式です。ここでTは型、vは変数です。これは、vを型Tに変換した後に、その結果のフィールドFieldにアクセスすることを意味します。

アドレス可能性 (Addressability)

Go言語において、ある式が「アドレス可能」であるとは、その式のメモリ上の位置(アドレス)を取得できることを意味します。アドレス可能である式に対しては、&演算子を使用してポインタを取得できます。例えば、変数や配列の要素、構造体のフィールドは通常アドレス可能です。しかし、定数、リテラル、関数呼び出しの結果、マップの要素などはアドレス可能ではありません。競合検出器は、メモリアクセスを監視するためにそのアドレスを必要とするため、アドレス可能でない式に対するアクセスは直接インストゥルメントできません。

OCONVNOP

Goコンパイラの内部表現において、OCONVNOPは「no-op conversion」(何もしない変換)を表すノードタイプです。これは、型変換が行われるが、実行時には実際の値の変更やコピーが発生しないようなケース(例えば、基底型が同じエイリアス型への変換など)で生成されることがあります。このコミットでは、T(v).Fieldのようなケースで、T(v)の部分がOCONVNOPとして表現されることがあり、これが競合検出器のインストゥルメンテーションを妨げていた可能性があります。

技術的詳細

このコミットの核心は、src/cmd/gc/racewalk.cファイルに追加されたmakeaddable関数と、そのcallinstr関数からの呼び出しです。

racewalk.cの役割

racewalk.cは、Goコンパイラのデータ競合検出器のインストゥルメンテーションロジックが含まれるファイルです。このファイル内の関数は、プログラムの抽象構文木(AST)を走査し、メモリ読み書き操作を特定します。そして、これらの操作の直前に、競合検出器のランタイム関数(racereadracewrite)への呼び出しを挿入します。これらのランタイム関数は、アクセスされたメモリのアドレスとサイズを記録し、他のゴルーチンからのアクセスと照合して競合を検出します。

makeaddable関数の導入

makeaddable関数は、nというノード(ASTの要素)を受け取り、そのメモリ位置がnと同じでありながら、Go言語の意味で「アドレス可能」なノードに変換することを目的としています。これは、cheapexprのような関数が引数のコピーを作成する可能性があるのとは異なり、元のメモリ位置を維持しつつアドレス可能性を確保します。

makeaddable関数は、以下のノードタイプに対して特別な処理を行います。

  • OINDEX (配列/スライス要素へのアクセス):
    • もしn->left->typeが固定長配列(isfixedarray)であれば、配列の基底部分(n->left)もアドレス可能にするために再帰的にmakeaddable(n->left)を呼び出します。これは、array[index]のようなアクセスで、array自体が型変換の結果である場合に重要になります。
  • ODOT (構造体フィールドへのアクセス) および OXDOT (ポインタ経由の構造体フィールドへのアクセス):
    • T(v).Fieldのようなケースでは、T(v)の部分がコンパイラ内部でOCONVNOPノードとして表現されることがあります。OCONVNOPは実行時に何もしない変換ですが、競合検出器にとっては「アドレス可能でない」と誤解される原因となっていました。
    • makeaddableは、n->left->op == OCONVNOPの場合、n->leftn->left->leftに直接置き換えます。これにより、T(v)という変換ノードをスキップし、元の変数vに直接アクセスする形にASTを書き換えます。これにより、v.Fieldという形になり、vがアドレス可能であれば、そのフィールドもアドレス可能と判断されるようになります。
    • その後、makeaddable(n->left)を再帰的に呼び出し、フィールドアクセス元のノード(変換を剥がした後のv)もアドレス可能であることを確認します。
  • ODOTPTR (ポインタ経由の構造体フィールドへのアクセス):
    • このケースでは、特に何もする必要がないと判断されています。ポインタ経由のアクセスは通常、既にアドレス可能であるためです。
  • default:
    • 上記以外のノードタイプでは、特に何も処理を行いません。

callinstr関数での利用

callinstr関数は、メモリ読み書き操作をインストゥルメントする主要な関数です。この関数内で、uintptraddr(n)を呼び出す前に、新しく追加されたmakeaddable(n)が呼び出されます。これにより、uintptraddr(メモリのアドレスを取得する関数)に渡されるノードnが、Go言語の意味でアドレス可能であることが保証されます。結果として、T(v).Fieldのようなケースでも、競合検出器が正しくメモリのアドレスを取得し、インストゥルメンテーションを行うことができるようになります。

テストケースの追加

src/pkg/runtime/race/testdata/comp_test.goには、この修正を検証するための新しいテストケースが追加されています。

  • TestRaceConv1, TestRaceConv2, TestRaceConv3, TestRaceConv4といったテスト関数が追加されています。
  • これらのテストは、P2 P, S2 S, X2 Xといった型エイリアスや構造体埋め込み、配列など、様々な型変換とフィールドアクセスの組み合わせを試しています。
  • 各テストは、ゴルーチン内でフィールドに書き込みを行い、メインゴルーチンで同じフィールドを読み込むことで、意図的にデータ競合を発生させています。
  • この修正が正しく機能していれば、これらのテストは競合検出器によってデータ競合が報告され、テストが成功する(競合が検出される)はずです。

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

src/cmd/gc/racewalk.c

@@ -489,6 +490,7 @@ callinstr(Node **np, NodeList **init, int wr, int skip)
 		*np = n;
 	}
 	n = treecopy(n);
+	makeaddable(n); // <-- ここで新しくmakeaddableが呼び出される
 	f = mkcall(wr ? "racewrite" : "raceread", T, init, uintptraddr(n));
 	*init = list(*init, f);
 	return 1;
@@ -496,6 +498,37 @@ callinstr(Node **np, NodeList **init, int wr, int skip)
 	return 0;
 }

+// makeaddable returns a node whose memory location is the
+// same as n, but which is addressable in the Go language
+// sense.
+// This is different from functions like cheapexpr that may make
+// a copy of their argument.
+static void
+makeaddable(Node *n)
+{
+	// The arguments to uintptraddr technically have an address but
+	// may not be addressable in the Go sense: for example, in the case
+	// of T(v).Field where T is a struct type and v is
+	// an addressable value.
+	switch(n->op) {
+	case OINDEX: // 配列/スライス要素アクセス
+		if(isfixedarray(n->left->type))
+			makeaddable(n->left); // 基底配列もアドレス可能にする
+		break;
+	case ODOT: // 構造体フィールドアクセス
+	case OXDOT: // ポインタ経由の構造体フィールドアクセス
+		// Turn T(v).Field into v.Field
+		if(n->left->op == OCONVNOP) // OCONVNOPノードをスキップ
+			n->left = n->left->left;
+		makeaddable(n->left); // フィールドアクセス元もアドレス可能にする
+		break;
+	case ODOTPTR: // ポインタ経由の構造体フィールドアクセス (別ケース)
+	default:
+		// nothing to do
+		break;
+	}
+}
+
 static Node*
 uintptraddr(Node *n)
 {

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

@@ -83,6 +83,60 @@ func TestRaceCompArray(t *testing.T) {
 	<-c
 }

+type P2 P // 型エイリアス
+type S2 S // 型エイリアス
+
+// 型変換後のフィールドアクセス (P(p).x) の競合検出テスト
+func TestRaceConv1(t *testing.T) {
+	c := make(chan bool, 1)
+	var p P2
+	go func() {
+		p.x = 1 // ゴルーチン内で書き込み
+		c <- true
+	}()
+	_ = P(p).x // メインゴルーチンで読み込み (型変換あり)
+	<-c
+}
+
+// ポインタと型変換後のフィールドアクセス (P(*ptr).x) の競合検出テスト
+func TestRaceConv2(t *testing.T) {
+	c := make(chan bool, 1)
+	var p P2
+	go func() {
+		p.x = 1
+		c <- true
+	}()
+	ptr := &p
+	_ = P(*ptr).x // ポインタデリファレンスと型変換あり
+	<-c
+}
+
+// 構造体埋め込みと型変換後のフィールドアクセス (P2(S(s).s1).x) の競合検出テスト
+func TestRaceConv3(t *testing.T) {
+	c := make(chan bool, 1)
+	var s S2
+	go func() {
+		s.s1.x = 1
+		c <- true
+	}()
+	_ = P2(S(s).s1).x // 複数の型変換と構造体フィールドアクセス
+	<-c
+}
+
+type X struct {
+	V [4]P // 配列を含む構造体
+}
+
+type X2 X
+
+// 配列要素と型変換後のフィールドアクセス (P2(X(x).V[1]).x) の競合検出テスト
+func TestRaceConv4(t *testing.T) {
+	c := make(chan bool, 1)
+	var x X2
+	go func() {
+		x.V[1].x = 1 // 配列要素への書き込み
+		c <- true
+	}()
+	_ = P2(X(x).V[1]).x // 配列要素アクセスと型変換
+	<-c
+}
+
 type Ptr struct {
 	s1, s2 *P
 }

コアとなるコードの解説

このコミットの主要な変更点は、Goコンパイラのracewalk.cファイルにmakeaddableという新しい静的関数が追加され、既存のcallinstr関数から呼び出されるようになったことです。

  1. makeaddable関数の目的:

    • この関数は、競合検出器がインストゥルメンテーションを行う際に、対象となるメモリ位置がGo言語のセマンティクスにおいて「アドレス可能」であることを保証するために導入されました。
    • 特に、T(v).Fieldのような式において、T(v)の部分がコンパイラ内部でOCONVNOP(何もしない型変換)として表現される場合、その結果がアドレス可能でないと誤って判断される問題がありました。
    • makeaddableは、ODOTOXDOT(フィールドアクセス)のノードを処理する際に、もしその左の子ノードがOCONVNOPであれば、そのOCONVNOPノードをスキップして、そのさらに左の子ノード(元の変数vに相当)を直接参照するようにASTを書き換えます。これにより、T(v).Fieldが実質的にv.Fieldとして扱われ、vがアドレス可能であれば、そのフィールドもアドレス可能と認識されるようになります。
    • また、OINDEX(配列/スライス要素アクセス)の場合も、基底となる配列が固定長配列であれば、その基底配列もアドレス可能にするために再帰的にmakeaddableを呼び出します。
  2. callinstr関数からの呼び出し:

    • callinstr関数は、Goプログラム内のメモリ読み書き操作を特定し、その操作の直前に競合検出器のランタイム関数(racereadまたはracewrite)への呼び出しを挿入する役割を担っています。
    • このコミットでは、uintptraddr(n)(メモリのアドレスを取得する関数)を呼び出す直前に、makeaddable(n)が呼び出されるようになりました。これにより、uintptraddrに渡されるnが常にアドレス可能であることが保証され、競合検出器が正しくメモリのアドレスを取得し、競合を検出できるようになります。
  3. テストケースの追加:

    • src/pkg/runtime/race/testdata/comp_test.goに追加された複数のTestRaceConv関数は、この修正が正しく機能していることを検証するためのものです。
    • これらのテストは、様々な種類の型変換(例: P2からPへの変換)とフィールドアクセスを組み合わせ、意図的にデータ競合を発生させています。
    • 修正が適用されていれば、これらのテストは競合検出器によって競合が報告され、テストが成功します。これにより、特定の型変換とフィールドアクセスのパターンにおける競合検出の漏れが解消されたことが確認できます。

この変更により、Goのデータ競合検出器は、より複雑なコードパターン、特に型変換が絡むフィールドアクセスにおいても、データ競合を正確に検出できるようになり、Goプログラムの並行処理の安全性が向上しました。

関連リンク

参考にした情報源リンク

このコミットは、Goコンパイラ(cmd/gc)におけるデータ競合検出器のインストゥルメンテーションに関するバグ修正です。具体的には、T(v).Fieldのようなセレクタ(型変換後のフィールドアクセス)に対して、競合検出器が正しく動作しない問題を解決します。この修正により、Goのデータ競合検出器がより堅牢になり、特定のコードパターンにおける競合状態を見逃すことがなくなります。

コミット

commit 6c4943cb51cc7c4b27233a7717d74742871f7faa
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date:   Wed May 15 01:25:20 2013 +0200

    cmd/gc: fix race instrumentation of selectors T(v).Field

    Fixes #5424.

    R=golang-dev, daniel.morsing, dvyukov, r
    CC=golang-dev
    https://golang.org/cl/9033048

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

https://github.com/golang/go/commit/6c4943cb51cc7c4b27233a7717d74742871f7faa

元コミット内容

このコミットは、Goコンパイラのcmd/gc部分におけるデータ競合検出器のインストゥルメンテーションに関する修正です。特に、T(v).Fieldという形式のセレクタ(型変換後のフィールドアクセス)に対して、競合検出器が正しく動作しない問題を修正します。これは、GoのIssue #5424で報告されたバグに対応するものです。

変更の背景

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

このコミット以前は、T(v).Fieldのような特定のコードパターン、すなわち型変換(T(v))の後にその結果の構造体のフィールドにアクセスする(.Field)場合、競合検出器がそのメモリアクセスを正しくインストゥルメント(計測)できていませんでした。インストゥルメンテーションとは、コンパイラが実行時にメモリアクセスを監視するための追加コードを挿入するプロセスを指します。正しくインストゥルメントされない場合、実際にデータ競合が発生していても検出器がそれを報告しないという問題が生じます。

Issue #5424では、この問題が具体的に報告されており、T(v).Fieldのような式が、競合検出器にとって「アドレス可能(addressable)」ではないと誤って判断されることが原因でした。Goの競合検出器は、メモリ位置のアドレスを取得してそのアクセスを記録するため、アドレス可能でない式に対してはインストゥルメンテーションを行うことができません。このコミットは、この特定のエッジケースを修正し、競合検出器の信頼性を向上させることを目的としています。

前提知識の解説

Go言語のデータ競合検出器 (Race Detector)

Goのデータ競合検出器は、Go 1.1で導入された強力なツールです。go run -racego build -racego test -raceなどのコマンドで有効にできます。有効にすると、コンパイラはプログラムのメモリアクセスを監視するための追加コードを挿入します。実行時に、この監視コードが並行アクセスを検出し、データ競合の可能性を報告します。これは、スレッドサニタイザー(ThreadSanitizer, TSan)という技術に基づいています。

cmd/gc

cmd/gcは、Go言語の公式コンパイラのバックエンド部分を指します。Goのソースコードを機械語に変換する主要な役割を担っています。競合検出器のインストゥルメンテーションは、このコンパイラのフェーズで行われます。具体的には、racewalk.cのようなファイルが、メモリアクセスを検出して競合検出器のランタイム関数呼び出しを挿入する役割を担っています。

セレクタ (Selectors)

Go言語において、セレクタは構造体のフィールドやメソッドにアクセスするための構文です。例えば、s.Fields.Method()のように使用されます。このコミットで問題となっているのは、T(v).Fieldのような形式です。ここでTは型、vは変数です。これは、vを型Tに変換した後に、その結果のフィールドFieldにアクセスすることを意味します。

アドレス可能性 (Addressability)

Go言語において、ある式が「アドレス可能」であるとは、その式のメモリ上の位置(アドレス)を取得できることを意味します。アドレス可能である式に対しては、&演算子を使用してポインタを取得できます。例えば、変数や配列の要素、構造体のフィールドは通常アドレス可能です。しかし、定数、リテラル、関数呼び出しの結果、マップの要素などはアドレス可能ではありません。競合検出器は、メモリアクセスを監視するためにそのアドレスを必要とするため、アドレス可能でない式に対するアクセスは直接インストゥルメントできません。

OCONVNOP

Goコンパイラの内部表現において、OCONVNOPは「no-op conversion」(何もしない変換)を表すノードタイプです。これは、型変換が行われるが、実行時には実際の値の変更やコピーが発生しないようなケース(例えば、基底型が同じエイリアス型への変換など)で生成されることがあります。このコミットでは、T(v).Fieldのようなケースで、T(v)の部分がOCONVNOPとして表現されることがあり、これが競合検出器のインストゥルメンテーションを妨げていた可能性があります。

技術的詳細

このコミットの核心は、src/cmd/gc/racewalk.cファイルに追加されたmakeaddable関数と、そのcallinstr関数からの呼び出しです。

racewalk.cの役割

racewalk.cは、Goコンパイラのデータ競合検出器のインストゥルメンテーションロジックが含まれるファイルです。このファイル内の関数は、プログラムの抽象構文木(AST)を走査し、メモリ読み書き操作を特定します。そして、これらの操作の直前に、競合検出器のランタイム関数(racereadracewrite)への呼び出しを挿入します。これらのランタイム関数は、アクセスされたメモリのアドレスとサイズを記録し、他のゴルーチンからのアクセスと照合して競合を検出します。

makeaddable関数の導入

makeaddable関数は、nというノード(ASTの要素)を受け取り、そのメモリ位置がnと同じでありながら、Go言語の意味で「アドレス可能」なノードに変換することを目的としています。これは、cheapexprのような関数が引数のコピーを作成する可能性があるのとは異なり、元のメモリ位置を維持しつつアドレス可能性を確保します。

makeaddable関数は、以下のノードタイプに対して特別な処理を行います。

  • OINDEX (配列/スライス要素へのアクセス):
    • もしn->left->typeが固定長配列(isfixedarray)であれば、配列の基底部分(n->left)もアドレス可能にするために再帰的にmakeaddable(n->left)を呼び出します。これは、array[index]のようなアクセスで、array自体が型変換の結果である場合に重要になります。
  • ODOT (構造体フィールドへのアクセス) および OXDOT (ポインタ経由の構造体フィールドへのアクセス):
    • T(v).Fieldのようなケースでは、T(v)の部分がコンパイラ内部でOCONVNOPノードとして表現されることがあります。OCONVNOPは実行時に何もしない変換ですが、競合検出器にとっては「アドレス可能でない」と誤解される原因となっていました。
    • makeaddableは、n->left->op == OCONVNOPの場合、n->leftn->left->leftに直接置き換えます。これにより、T(v)という変換ノードをスキップし、元の変数vに直接アクセスする形にASTを書き換えます。これにより、v.Fieldという形になり、vがアドレス可能であれば、そのフィールドもアドレス可能と判断されるようになります。
    • その後、makeaddable(n->left)を再帰的に呼び出し、フィールドアクセス元のノード(変換を剥がした後のv)もアドレス可能であることを確認します。
  • ODOTPTR (ポインタ経由の構造体フィールドへのアクセス):
    • このケースでは、特に何もする必要がないと判断されています。ポインタ経由のアクセスは通常、既にアドレス可能であるためです。
  • default:
    • 上記以外のノードタイプでは、特に何も処理を行いません。

callinstr関数での利用

callinstr関数は、メモリ読み書き操作をインストゥルメントする主要な関数です。この関数内で、uintptraddr(n)を呼び出す前に、新しく追加されたmakeaddable(n)が呼び出されます。これにより、uintptraddr(メモリのアドレスを取得する関数)に渡されるノードnが、Go言語の意味でアドレス可能であることが保証されます。結果として、T(v).Fieldのようなケースでも、競合検出器が正しくメモリのアドレスを取得し、インストゥルメンテーションを行うことができるようになります。

テストケースの追加

src/pkg/runtime/race/testdata/comp_test.goには、この修正を検証するための新しいテストケースが追加されています。

  • TestRaceConv1, TestRaceConv2, TestRaceConv3, TestRaceConv4といったテスト関数が追加されています。
  • これらのテストは、P2 P, S2 S, X2 Xといった型エイリアスや構造体埋め込み、配列など、様々な型変換とフィールドアクセスの組み合わせを試しています。
  • 各テストは、ゴルーチン内でフィールドに書き込みを行い、メインゴルーチンで同じフィールドを読み込むことで、意図的にデータ競合を発生させています。
  • この修正が正しく機能していれば、これらのテストは競合検出器によってデータ競合が報告され、テストが成功する(競合が検出される)はずです。

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

src/cmd/gc/racewalk.c

@@ -489,6 +490,7 @@ callinstr(Node **np, NodeList **init, int wr, int skip)
 		*np = n;
 	}
 	n = treecopy(n);
+	makeaddable(n); // <-- ここで新しくmakeaddableが呼び出される
 	f = mkcall(wr ? "racewrite" : "raceread", T, init, uintptraddr(n));
 	*init = list(*init, f);
 	return 1;
@@ -496,6 +498,37 @@ callinstr(Node **np, NodeList **init, int wr, int skip)
 	return 0;
 }

+// makeaddable returns a node whose memory location is the
+// same as n, but which is addressable in the Go language
+// sense.
+// This is different from functions like cheapexpr that may make
+// a copy of their argument.
+static void
+makeaddable(Node *n)
+{
+	// The arguments to uintptraddr technically have an address but
+	// may not be addressable in the Go sense: for example, in the case
+	// of T(v).Field where T is a struct type and v is
+	// an addressable value.
+	switch(n->op) {
+	case OINDEX: // 配列/スライス要素アクセス
+		if(isfixedarray(n->left->type))
+			makeaddable(n->left); // 基底配列もアドレス可能にする
+		break;
+	case ODOT: // 構造体フィールドアクセス
+	case OXDOT: // ポインタ経由の構造体フィールドアクセス
+		// Turn T(v).Field into v.Field
+		if(n->left->op == OCONVNOP) // OCONVNOPノードをスキップ
+			n->left = n->left->left;
+		makeaddable(n->left); // フィールドアクセス元もアドレス可能にする
+		break;
+	case ODOTPTR: // ポインタ経由の構造体フィールドアクセス (別ケース)
+	default:
+		// nothing to do
+		break;
+	}
+}
+
 static Node*
 uintptraddr(Node *n)
 {

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

@@ -83,6 +83,60 @@ func TestRaceCompArray(t *testing.T) {
 	<-c
 }

+type P2 P // 型エイリアス
+type S2 S // 型エイリアス
+
+// 型変換後のフィールドアクセス (P(p).x) の競合検出テスト
+func TestRaceConv1(t *testing.T) {
+	c := make(chan bool, 1)
+	var p P2
+	go func() {
+		p.x = 1 // ゴルーチン内で書き込み
+		c <- true
+	}()
+	_ = P(p).x // メインゴルーチンで読み込み (型変換あり)
+	<-c
+}
+
+// ポインタと型変換後のフィールドアクセス (P(*ptr).x) の競合検出テスト
+func TestRaceConv2(t *testing.T) {
+	c := make(chan bool, 1)
+	var p P2
+	go func() {
+		p.x = 1
+		c <- true
+	}()
+	ptr := &p
+	_ = P(*ptr).x // ポインタデリファレンスと型変換あり
+	<-c
+}
+
+// 構造体埋め込みと型変換後のフィールドアクセス (P2(S(s).s1).x) の競合検出テスト
+func TestRaceConv3(t *testing.T) {
+	c := make(chan bool, 1)
+	var s S2
+	go func() {
+		s.s1.x = 1
+		c <- true
+	}()
+	_ = P2(S(s).s1).x // 複数の型変換と構造体フィールドアクセス
+	<-c
+}
+
+type X struct {
+	V [4]P // 配列を含む構造体
+}
+
+type X2 X
+
+// 配列要素と型変換後のフィールドアクセス (P2(X(x).V[1]).x) の競合検出テスト
+func TestRaceConv4(t *testing.T) {
+	c := make(chan bool, 1)
+	var x X2
+	go func() {
+		x.V[1].x = 1 // 配列要素への書き込み
+		c <- true
+	}()
+	_ = P2(X(x).V[1]).x // 配列要素アクセスと型変換
+	<-c
+}
+
 type Ptr struct {
 	s1, s2 *P
 }

コアとなるコードの解説

このコミットの主要な変更点は、Goコンパイラのracewalk.cファイルにmakeaddableという新しい静的関数が追加され、既存のcallinstr関数から呼び出されるようになったことです。

  1. makeaddable関数の目的:

    • この関数は、競合検出器がインストゥルメンテーションを行う際に、対象となるメモリ位置がGo言語のセマンティクスにおいて「アドレス可能」であることを保証するために導入されました。
    • 特に、T(v).Fieldのような式において、T(v)の部分がコンパイラ内部でOCONVNOP(何もしない型変換)として表現される場合、その結果がアドレス可能でないと誤って判断される問題がありました。
    • makeaddableは、ODOTOXDOT(フィールドアクセス)のノードを処理する際に、もしその左の子ノードがOCONVNOPであれば、そのOCONVNOPノードをスキップして、そのさらに左の子ノード(元の変数vに相当)を直接参照するようにASTを書き換えます。これにより、T(v).Fieldが実質的にv.Fieldとして扱われ、vがアドレス可能であれば、そのフィールドもアドレス可能と認識されるようになります。
    • また、OINDEX(配列/スライス要素アクセス)の場合も、基底となる配列が固定長配列であれば、その基底配列もアドレス可能にするために再帰的にmakeaddableを呼び出します。
  2. callinstr関数からの呼び出し:

    • callinstr関数は、Goプログラム内のメモリ読み書き操作を特定し、その操作の直前に競合検出器のランタイム関数(racereadまたはracewrite)への呼び出しを挿入する役割を担っています。
    • このコミットでは、uintptraddr(n)(メモリのアドレスを取得する関数)を呼び出す直前に、makeaddable(n)が呼び出されるようになりました。これにより、uintptraddrに渡されるnが常にアドレス可能であることが保証され、競合検出器が正しくメモリのアドレスを取得し、競合を検出できるようになります。
  3. テストケースの追加:

    • src/pkg/runtime/race/testdata/comp_test.goに追加された複数のTestRaceConv関数は、この修正が正しく機能していることを検証するためのものです。
    • これらのテストは、様々な種類の型変換(例: P2からPへの変換)とフィールドアクセスを組み合わせ、意図的にデータ競合を発生させています。
    • 修正が適用されていれば、これらのテストは競合検出器によって競合が報告され、テストが成功します。これにより、特定の型変換とフィールドアクセスのパターンにおける競合検出の漏れが解消されたことが確認できます。

この変更により、Goのデータ競合検出器は、より複雑なコードパターン、特に型変換が絡むフィールドアクセスにおいても、データ競合を正確に検出できるようになり、Goプログラムの並行処理の安全性が向上しました。

関連リンク

参考にした情報源リンク