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

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

このコミットは、Goランタイムのデータ競合検出器(Race Detector)におけるバグ修正に関するものです。具体的には、KindNoPointersビットが設定されている型(ポインタを含まない型)を比較する際に、データ競合検出器が誤ったチェックを行う問題を修正しています。これにより、データ競合検出の正確性が向上し、偽陽性(false positive)の報告が減少することが期待されます。

コミット

commit f59ea4e58b77e4540e87e42dc9192b8d424adf6b
Author: Keith Randall <khr@golang.org>
Date:   Sat Jan 4 08:43:17 2014 -0800

    runtime: Fix race detector checks to ignore KindNoPointers bit
    when comparing kinds.
    
    R=dvyukov, dave, khr
    CC=golang-codereviews
    https://golang.org/cl/41660045

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

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

元コミット内容

このコミットの目的は、Goランタイムのデータ競合検出器が、型の種類(Kind)を比較する際に、KindNoPointersビットを無視するように修正することです。これにより、ポインタを含まない型に対するデータ競合チェックが正しく行われるようになります。

変更の背景

Goのデータ競合検出器は、並行処理におけるメモリへの同時アクセスによって発生するデータ競合を検出するための強力なツールです。データ競合は、複数のゴルーチンが同じメモリ位置に同時にアクセスし、少なくとも1つのアクセスが書き込みである場合に発生し、プログラムの予測不能な動作やバグの原因となります。

Goの型システムでは、各型にはその種類(Kind)が定義されており、例えばintKindIntstructKindStructといった具合です。Goランタイム内部では、これらのKind情報に加えて、その型がポインタを含まないかどうかを示すKindNoPointersのようなビットフラグが付加されることがあります。これは、ガベージコレクションやデータ競合検出器がメモリをスキャンする際に、ポインタの有無によって処理を最適化するためです。

このコミット以前のデータ競合検出器は、型のKindを比較する際に、このKindNoPointersビットを適切に無視していませんでした。その結果、ポインタを含まない型(例えば、純粋な数値の配列や構造体)であっても、KindNoPointersビットが設定されているために、データ競合検出器がその型をポインタを含む型として誤って解釈し、不必要な、あるいは誤った競合チェックを実行してしまう可能性がありました。特に、KindArrayKindStructのような複合型の場合、その内部にポインタが含まれているかどうかで競合検出のロジックが変わるため、この誤解釈は問題となります。

この修正は、データ競合検出器が型のKindを評価する際に、KindNoPointersビットをマスクすることで、型の本質的な種類のみに基づいて判断するようにし、検出の正確性を向上させることを目的としています。

前提知識の解説

Goの型とreflect.Kind

Go言語では、すべての値は特定の型を持ちます。Goの標準ライブラリにはreflectパッケージがあり、実行時にGoのプログラムの構造を検査したり、値を操作したりする機能を提供します。reflect.Kindは、Goの型の基本的なカテゴリを表す列挙型です。例えば、reflect.Intreflect.Stringreflect.Structreflect.Arrayreflect.Ptrなどがあります。

Goランタイムの内部では、これらのKind情報に加えて、型の特性を示す追加のビットフラグが使用されることがあります。KindNoPointersは、その型がポインタを含まないことを示す内部的なフラグであると考えられます。これは、ガベージコレクタがメモリをスキャンする際に、ポインタを含まない領域をスキップしてパフォーマンスを向上させるためや、データ競合検出器がポインタの有無によって異なるチェックを行うために利用されます。

データ競合(Data Race)

データ競合は、並行プログラミングにおける深刻なバグの一種です。以下の3つの条件がすべて満たされたときに発生します。

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

データ競合が発生すると、プログラムの実行結果が非決定論的になり、デバッグが非常に困難になります。

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

Goには、プログラム実行中にデータ競合を検出するための組み込みツールであるデータ競合検出器があります。これは、go run -racego build -racego test -raceなどのコマンドで有効にできます。データ競合検出器は、メモリへのアクセスを監視し、競合のパターンを検出すると警告を報告します。このツールは、Goプログラムの並行処理のバグを見つける上で非常に効果的です。

データ競合検出器は、メモリへの読み書き操作をフックし、各メモリ位置に対して「最後に誰がアクセスしたか」という情報を追跡します。異なるゴルーチンからの競合するアクセス(特に書き込みを含む場合)が検出されると、競合が報告されます。

runtime·racereadpc, runtime·racewritepc, runtime·racereadrangepc, runtime·racewriterangepc

これらはGoランタイム内部で使用される関数で、データ競合検出器がメモリへのアクセスを記録するために呼び出されます。

  • runtime·racereadpc(addr, callpc, pc): 単一のメモリ位置addrへの読み込みアクセスを記録します。callpcは呼び出し元のプログラムカウンタ、pcはアクセスが発生したプログラムカウンタです。
  • runtime·racewritepc(addr, callpc, pc): 単一のメモリ位置addrへの書き込みアクセスを記録します。
  • runtime·racereadrangepc(addr, sz, callpc, pc): addrからszバイトの範囲への読み込みアクセスを記録します。配列や構造体など、連続したメモリ領域へのアクセスに使用されます。
  • runtime·racewriterangepc(addr, sz, callpc, pc): addrからszバイトの範囲への書き込みアクセスを記録します。

これらの関数は、アクセスされるデータの種類(単一の値か、配列/構造体のような範囲か)によって使い分けられていました。

技術的詳細

このコミットの核心は、データ競合検出器が型のKindを判断する際に、KindNoPointersビットを無視するように変更した点です。

Goランタイム内部では、型のKindは単なる列挙値ではなく、追加のビットフラグを含むuint8型の値として表現されることがあります。KindNoPointersは、その型がポインタを含まないことを示すビットフラグです。例えば、KindIntはポインタを含まないため、KindInt | KindNoPointersのような形で表現される可能性があります(これは概念的な説明であり、実際のビット表現はランタイムの実装に依存します)。

データ競合検出器は、アクセスされるデータが配列や構造体のような複合型である場合、その全体を範囲としてチェックする必要があります。一方、プリミティブ型(int, boolなど)のような単一の値である場合は、単一のメモリ位置としてチェックします。この判断は、型のKindに基づいて行われます。

以前の実装では、t->key->kind == KindArray || t->key->kind == KindStructのような条件で型の種類をチェックしていました。しかし、もしt->key->kindKindArray | KindNoPointersのような値であった場合、この比較はKindArrayとは一致せず、誤って単一のメモリ位置として扱われてしまう可能性がありました。

この修正では、runtime/race.cruntime·racereadobjectpcruntime·racewriteobjectpcという新しいヘルパー関数が導入されました。これらの関数は、引数としてType *t(型情報)を受け取ります。関数内で、t->kindからKindNoPointersビットをマスクアウトすることで、純粋なKind値を取得します。

kind = t->kind & ~KindNoPointers;

このkind変数を使って、KindArrayまたはKindStructであるかを判断し、適切な競合検出関数(rangeaccessまたはmemoryaccess)を呼び出すように変更されました。

  • KindArrayまたはKindStructの場合: rangeaccess(addr, t->size, ...)を呼び出し、型のサイズ全体を対象とした範囲アクセスとしてチェックします。
  • それ以外の場合: memoryaccess(addr, ...)を呼び出し、単一のメモリ位置アクセスとしてチェックします。

この変更により、hashmap.c内のruntime·mapaccess1, runtime·mapaccess2, reflect·mapaccess, runtime·mapassign1, runtime·mapdelete, reflect·mapassign, reflect·mapdeleteといったマップ操作に関連する関数が、直接KindArrayKindStructをチェックする代わりに、新しく導入されたruntime·racereadobjectpcruntime·racewriteobjectpcを呼び出すように変更されました。これにより、マップのキーや要素の型がポインタを含まない場合でも、データ競合検出器が正しく動作するようになります。

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

src/pkg/runtime/hashmap.c

runtime·mapaccess1, runtime·mapaccess2, reflect·mapaccess, runtime·mapassign1, runtime·mapdelete, reflect·mapassign, reflect·mapdelete の各関数内で、データ競合検出器の呼び出し部分が変更されています。

変更前:

        if(t->key->kind == KindArray || t->key->kind == KindStruct)
                runtime·racereadrangepc(ak, t->key->size, runtime·getcallerpc(&t), runtime·mapaccess1);
        else
                runtime·racereadpc(ak, runtime·getcallerpc(&t), runtime·mapaccess1);

変更後:

        runtime·racereadobjectpc(ak, t->key, runtime·getcallerpc(&t), runtime·mapaccess1);

同様に、mapassign系の関数ではキーと要素の両方に対して変更が加えられています。

src/pkg/runtime/race.c

新しい関数 runtime·racewriteobjectpcruntime·racereadobjectpc が追加されています。

void
runtime·racewriteobjectpc(void *addr, Type *t, void *callpc, void *pc)
{
	uint8 kind;

	kind = t->kind & ~KindNoPointers; // KindNoPointers ビットをマスクアウト
	if(kind == KindArray || kind == KindStruct)
		rangeaccess(addr, t->size, (uintptr)callpc, (uintptr)pc, true);
	else
		memoryaccess(addr, (uintptr)callpc, (uintptr)pc, true);
}

void
runtime·racereadobjectpc(void *addr, Type *t, void *callpc, void *pc)
{
	uint8 kind;

	kind = t->kind & ~KindNoPointers; // KindNoPointers ビットをマスクアウト
	if(kind == KindArray || kind == KindStruct)
		rangeaccess(addr, t->size, (uintptr)callpc, (uintptr)pc, false);
	else
		memoryaccess(addr, (uintptr)callpc, (uintptr)pc, false);
}

また、必要なヘッダーファイルとして type.htypekind.h がインクルードされています。

src/pkg/runtime/race.h

新しく追加された runtime·racereadobjectpcruntime·racewriteobjectpc の関数プロトタイプが宣言されています。

void	runtime·racereadobjectpc(void *addr, Type *t, void *callpc, void *pc);
void	runtime·racewriteobjectpc(void *addr, Type *t, void *callpc, void *pc);

コアとなるコードの解説

このコミットの主要な変更点は、データ競合検出器が型の種類を判断するロジックを、より堅牢なものにしたことです。

以前のコードでは、マップのキーや要素の型が配列(KindArray)または構造体(KindStruct)であるかどうかを直接t->key->kind == KindArrayのように比較していました。しかし、Goランタイム内部では、型のKind情報にKindNoPointersというビットフラグが付加されることがあります。このフラグは、その型がポインタを含まないことを示します。例えば、[10]intのような配列はポインタを含まないため、そのkind値はKindArray | KindNoPointersのような形になる可能性があります。

この場合、t->key->kind == KindArrayという比較はfalseとなり、データ競合検出器は配列全体ではなく、単一のメモリ位置としてアクセスを記録してしまいます。これは、配列全体へのアクセスを監視すべき場合に、不正確な競合検出を引き起こす可能性があります。

新しいruntime·racereadobjectpcおよびruntime·racewriteobjectpc関数は、この問題を解決します。これらの関数は、引数としてType *tを受け取り、まずt->kind & ~KindNoPointersという操作でKindNoPointersビットをマスクアウトします。これにより、KindNoPointersビットが設定されていても、型の基本的な種類(KindArrayKindStructなど)を正しく抽出できます。

抽出されたkind値がKindArrayまたはKindStructである場合、これらの関数はrangeaccessを呼び出し、型のサイズ全体を対象とした競合チェックを行います。それ以外の場合は、memoryaccessを呼び出し、単一のメモリ位置に対する競合チェックを行います。

この変更により、hashmap.c内のマップ操作関数は、キーや要素の型がどのような内部表現であっても、データ競合検出器に正しいアクセス範囲を通知できるようになりました。これにより、データ競合検出器の精度が向上し、特にポインタを含まない複合型に対する偽陽性や見逃しが減少することが期待されます。

関連リンク

参考にした情報源リンク