[インデックス 17904] ファイルの概要
このコミットは、Goランタイムにおけるマップの実装が変更された際に発生した、レース検出器の不具合を修正するものです。具体的には、マップのキーと値がポインタ経由で渡される場合に、レース検出器が正しく動作しない問題を解決します。マップの実装が、スタック上のスロットからではなく、任意のメモリ位置からキーと値を読み取るようになったため、レース検出器にそのアクセスを明示的に通知する必要が生じました。
コミット
commit c0f229457731daa170fea3c8eb2c4f4c363266d3
Author: Keith Randall <khr@golang.org>
Date: Mon Dec 2 18:03:25 2013 -0800
runtime: fix race detector when map keys/values are passed by pointer.
Now that the map implementation is reading the keys and values from
arbitrary memory (instead of from stack slots), it needs to tell the
race detector when it does so.
Fixes #6875.
R=golang-dev, dave
CC=golang-dev
https://golang.org/cl/36360043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/c0f229457731daa170fea3c8eb2c4f4c363266d3
元コミット内容
runtime: fix race detector when map keys/values are passed by pointer.
Now that the map implementation is reading the keys and values from
arbitrary memory (instead of from stack slots), it needs to tell the
race detector when it does so.
Fixes #6875.
R=golang-dev, dave
CC=golang-dev
https://golang.org/cl/36360043
変更の背景
この変更の背景には、Go言語のマップ(map
)の実装における内部的な変更があります。以前のGoのバージョンでは、マップのキーや値は、関数呼び出しの際にスタック上に一時的にコピーされる「スタック上のスロット」から読み取られることがありました。しかし、パフォーマンス最適化やメモリ管理の改善のために、マップの実装が変更され、キーや値が「任意のメモリ」(ヒープなど、スタック以外の場所)から直接読み取られるようになりました。
この変更自体は効率的ですが、Goのレース検出器(Race Detector)との間で問題を引き起こしました。レース検出器は、複数のゴルーチンが共有メモリに同時にアクセスし、少なくとも一方が書き込み操作である場合に発生するデータ競合(data race)を検出するために設計されています。レース検出器が正しく機能するためには、メモリへのすべての読み書き操作を監視する必要があります。
マップの実装が変更され、キーや値がスタック以外のメモリから読み取られるようになったことで、レース検出器はこれらのメモリアクセスを自動的に追跡できなくなりました。その結果、マップ操作中に発生する可能性のあるデータ競合が検出されなくなり、Goプログラムの信頼性とデバッグの困難さが増す可能性がありました。このコミットは、この問題を解決し、マップ操作におけるレース検出器の正確性を回復することを目的としています。具体的には、Go issue #6875 で報告された問題に対応しています。
前提知識の解説
Goのレース検出器 (Race Detector)
Goのレース検出器は、並行処理におけるデータ競合を検出するための強力なツールです。データ競合は、複数のゴルーチンが同じメモリ位置に同時にアクセスし、そのうち少なくとも1つが書き込み操作である場合に発生します。このような競合は、プログラムの予測不能な動作やバグの原因となります。
レース検出器は、プログラムの実行中にメモリアクセスを監視し、競合のパターンを特定します。検出器は、各メモリ操作(読み込みまたは書き込み)に対して、それがどのゴルーチンによって、どのスタックトレースから行われたかを記録します。そして、競合する可能性のあるアクセスパターンを検出すると、警告を生成します。
レース検出器は、go run -race
、go build -race
、go test -race
などのコマンドで有効にできます。有効にすると、プログラムの実行速度は低下しますが、並行処理のバグを見つける上で非常に価値があります。
Goのマップ (map)
Goのマップは、キーと値のペアを格納するための組み込みのデータ構造です。ハッシュテーブルとして実装されており、キーを使って高速に値にアクセスできます。マップは参照型であり、内部的にはポインタを通じてヒープ上のデータ構造を指しています。
マップの操作(要素の追加、取得、削除)は、内部的に複雑なメモリ操作を伴います。特に、キーや値が構造体や配列などの複合型である場合、それらのデータはマップの内部構造にコピーされたり、マップの内部から読み出されたりします。
スタックとヒープ、および「任意のメモリ」
- スタック (Stack): 関数呼び出しやローカル変数の格納に使われるメモリ領域です。LIFO(後入れ先出し)の構造を持ち、高速なアクセスが可能です。関数の実行が終了すると、その関数が使用していたスタック領域は自動的に解放されます。
- ヒープ (Heap): プログラムの実行中に動的にメモリを割り当てるために使われるメモリ領域です。スタックとは異なり、ヒープに割り当てられたメモリは、明示的に解放されるか、ガベージコレクタによって回収されるまで保持されます。Goでは、
make
やnew
で作成されたデータ構造や、関数間で共有される可能性のあるデータはヒープに割り当てられることが多いです。 - 「任意のメモリ」 (Arbitrary Memory): この文脈では、スタック以外のメモリ領域、特にヒープを指します。マップの実装が変更され、キーや値がスタックに一時的にコピーされるのではなく、ヒープ上のマップの内部構造から直接アクセスされるようになったことを意味します。
技術的詳細
このコミットの技術的な核心は、Goのマップ実装が変更されたことにより、レース検出器がマップのキーと値へのアクセスを正しく追跡できなくなった問題に対処することです。
以前のマップ実装では、マップのキーや値が関数呼び出しの際にスタック上のスロットにコピーされることがありました。レース検出器は、スタック上のメモリへのアクセスを自動的に監視するメカニズムを持っていたため、これらの操作は特に明示的な介入なしに検出されていました。
しかし、マップの実装が変更され、キーや値がスタックではなく、ヒープ上のマップの内部データ構造から直接読み書きされるようになりました。この「任意のメモリ」へのアクセスは、レース検出器が自動的に追跡する範囲外となる場合がありました。その結果、複数のゴルーチンが同時にマップのキーや値にアクセスし、そのうち少なくとも一方が書き込み操作であるようなデータ競合が発生しても、レース検出器がそれを報告しないという問題が生じました。
この問題を解決するために、runtime·mapaccess1
(マップからの値の読み込み)、runtime·mapaccess2
(マップからの値と存在フラグの読み込み)、runtime·mapassign1
(マップへの値の書き込み)、runtime·mapdelete
(マップからの値の削除)といったマップ操作を行うランタイム関数に、明示的なレース検出器への通知が追加されました。
具体的には、runtime·racereadpc
と runtime·racewritepc
という関数が呼び出されます。
runtime·racereadpc(addr, callerpc, funcpc)
:addr
で指定されたメモリ位置からの読み込み操作をレース検出器に通知します。callerpc
は呼び出し元のプログラムカウンタ、funcpc
は現在の関数のプログラムカウンタです。runtime·racewritepc(addr, callerpc, funcpc)
:addr
で指定されたメモリ位置への書き込み操作をレース検出器に通知します。
これらの関数を呼び出すことで、マップのキーや値がヒープ上の「任意のメモリ」からアクセスされる場合でも、レース検出器がその操作を認識し、潜在的なデータ競合を正確に検出できるようになります。これにより、Goプログラムの並行処理における安全性が向上します。
コアとなるコードの変更箇所
変更は src/pkg/runtime/hashmap.c
ファイルに対して行われています。
--- a/src/pkg/runtime/hashmap.c
+++ b/src/pkg/runtime/hashmap.c
@@ -991,9 +991,10 @@ reflect·makemap(MapType *t, Hmap *ret)
void
runtime·mapaccess1(MapType *t, Hmap *h, byte *ak, byte *av)
{
- if(raceenabled && h != nil)
+ if(raceenabled && h != nil) {
runtime·racereadpc(h, runtime·getcallerpc(&t), runtime·mapaccess1);
-
+ runtime·racereadpc(ak, runtime·getcallerpc(&t), runtime·mapaccess1);
+ }
if(h == nil || h->count == 0) {
av = t->elem->zero;
} else {
@@ -1021,8 +1022,10 @@ runtime·mapaccess1(MapType *t, Hmap *h, byte *ak, byte *av)
void
runtime·mapaccess2(MapType *t, Hmap *h, byte *ak, byte *av, bool pres)
{
- if(raceenabled && h != nil)
+ if(raceenabled && h != nil) {
runtime·racereadpc(h, runtime·getcallerpc(&t), runtime·mapaccess2);
+ runtime·racereadpc(ak, runtime·getcallerpc(&t), runtime·mapaccess2);
+ }
if(h == nil || h->count == 0) {
av = t->elem->zero;
@@ -1097,8 +1100,11 @@ runtime·mapassign1(MapType *t, Hmap *h, byte *ak, byte *av)
if(h == nil)
runtime·panicstring("assignment to entry in nil map");
- if(raceenabled)
+ if(raceenabled) {
runtime·racewritepc(h, runtime·getcallerpc(&t), runtime·mapassign1);
+ runtime·racereadpc(ak, runtime·getcallerpc(&t), runtime·mapassign1);
+ runtime·racereadpc(av, runtime·getcallerpc(&t), runtime·mapassign1);
+ }
hash_insert(t, h, ak, av);
@@ -1121,8 +1127,10 @@ runtime·mapdelete(MapType *t, Hmap *h, byte *ak)
if(h == nil)
return;
- if(raceenabled)
+ if(raceenabled) {
runtime·racewritepc(h, runtime·getcallerpc(&t), runtime·mapdelete);
+ runtime·racereadpc(ak, runtime·getcallerpc(&t), runtime·mapdelete);
+ }
hash_remove(t, h, ak);
コアとなるコードの解説
このコミットでは、Goランタイムのマップ操作に関連する4つの関数 runtime·mapaccess1
, runtime·mapaccess2
, runtime·mapassign1
, runtime·mapdelete
に、レース検出器への明示的な通知が追加されています。
各変更点について詳しく見ていきます。
-
runtime·mapaccess1
(マップからの値の読み込み)- 変更前:
h
(マップヘッダ) への読み込み操作のみをレース検出器に通知していました。 - 変更後:
ak
(キーのポインタ) への読み込み操作もruntime·racereadpc
を使ってレース検出器に通知するようになりました。これは、マップのキーが任意のメモリから読み取られるようになったため、そのアクセスも監視する必要があるためです。
- 変更前:
-
runtime·mapaccess2
(マップからの値と存在フラグの読み込み)- 変更前:
h
(マップヘッダ) への読み込み操作のみをレース検出器に通知していました。 - 変更後:
ak
(キーのポインタ) への読み込み操作もruntime·racereadpc
を使ってレース検出器に通知するようになりました。mapaccess1
と同様の理由です。
- 変更前:
-
runtime·mapassign1
(マップへの値の書き込み)- 変更前:
h
(マップヘッダ) への書き込み操作のみをレース検出器に通知していました。 - 変更後:
h
(マップヘッダ) への書き込み操作は引き続きruntime·racewritepc
で通知されます。ak
(キーのポインタ) への読み込み操作をruntime·racereadpc
で通知します。マップに値を割り当てる際、キーの値を読み取る必要があるためです。av
(値のポインタ) への読み込み操作をruntime·racereadpc
で通知します。同様に、割り当てる値のデータを読み取る必要があるためです。
- 変更前:
-
runtime·mapdelete
(マップからの値の削除)- 変更前:
h
(マップヘッダ) への書き込み操作のみをレース検出器に通知していました。 - 変更後:
h
(マップヘッダ) への書き込み操作は引き続きruntime·racewritepc
で通知されます。ak
(キーのポインタ) への読み込み操作をruntime·racereadpc
で通知します。マップから要素を削除する際、削除対象のキーを読み取る必要があるためです。
- 変更前:
これらの変更により、マップのキーや値がポインタ経由でアクセスされる場合でも、レース検出器がこれらのメモリ操作を正確に捕捉し、データ競合の検出精度が向上しました。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/c0f229457731daa170fea3c8eb2c4f4c363266d3
- Go Issue #6875: https://golang.org/issue/6875
- Go CL 36360043: https://golang.org/cl/36360043
参考にした情報源リンク
- Go Race Detector Documentation (公式ドキュメントや関連ブログ記事など、具体的なURLがあればここに記載)
- Go Map Implementation Details (Goのマップの内部実装に関する情報源があればここに記載)
- Stack vs Heap in Go (Goにおけるスタックとヒープに関する情報源があればここに記載)