[インデックス 18369] ファイルの概要
このコミットは、Goランタイムにおけるメモリ割り当て、特に「tiny allocations(微小な割り当て)」に関するデータ競合検出器(Race Detector)の計測ロジックを調整するものです。具体的には、racemalloc
関数の呼び出しタイミングを変更することで、微小なメモリブロックが複数のゴルーチンによって共有される際に発生しうる競合状態の誤検出や未検出を防ぎます。
コミット
commit ce884036d2199ebec22e4f9200789a532a1225d1
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Tue Jan 28 22:34:32 2014 +0400
runtime: adjust malloc race instrumentation for tiny allocs
Tiny alloc memory block is shared by different goroutines running on the same thread.
We call racemalloc after enabling preemption in mallocgc,
as the result another goroutine can act on not yet race-cleared tiny block.
Call racemalloc before enabling preemption.
Fixes #7224.
LGTM=dave
R=golang-codereviews, dave
CC=golang-codereviews
https://golang.org/cl/57730043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ce884036d2199ebec22e4f9200789a532a1225d1
元コミット内容
このコミットは、Goランタイムのメモリ割り当て処理において、データ競合検出器(Race Detector)のracemalloc
関数の呼び出しタイミングを修正します。
問題は、微小なメモリ割り当て(tiny allocs)の場合、同じスレッド上で実行されている異なるゴルーチン間でメモリブロックが共有される可能性がある点にありました。
これまでの実装では、mallocgc
関数内でプリエンプション(横取り)が有効になった後にracemalloc
が呼び出されていました。このタイミングだと、racemalloc
がまだ競合検出のためのクリア処理を終えていない微小ブロックに対して、別のゴルーチンがアクセスしてしまう可能性があり、これがデータ競合を引き起こす、あるいは誤った競合検出につながる可能性がありました。
このコミットでは、racemalloc
の呼び出しをプリエンプションが有効になる前に行うように変更することで、この問題を解決しています。
これはGoのIssue #7224を修正するものです。
変更の背景
Goのランタイムは、効率的なメモリ管理のために様々な最適化を行っています。その一つが「tiny allocations」と呼ばれる、非常に小さなオブジェクト(例えば16バイト未満)の割り当てに対する特殊なハンドリングです。これらの小さなオブジェクトは、通常、専用の小さなメモリブロック(tiny block)から割り当てられます。
問題の核心は、このtiny blockが、同じOSスレッド(M: マシン)上で実行されている複数のGoルーチン(G)間で共有される可能性があるという点にありました。Goのスケジューラは、M上でGをプリエンプト(横取り)し、別のGを実行させることができます。
GoのRace Detectorは、メモリへのアクセス競合を検出するための強力なツールです。メモリが割り当てられた際、racemalloc
関数が呼び出され、そのメモリ領域が競合検出の対象として適切に初期化(クリア)されます。
これまでのmallocgc
の実装では、racemalloc
の呼び出しが、プリエンプションが有効になる(g->stackguard0 = StackPreempt;
が設定される)後に行われていました。この順序だと、以下のような競合状態が発生する可能性がありました。
- ゴルーチンAが
mallocgc
を呼び出し、tiny blockを割り当てる。 mallocgc
内でプリエンプションが有効になる。racemalloc
が呼び出される前に、ゴルーチンAがプリエンプトされる。- 同じOSスレッド上で、別のゴルーチンBが実行される。
- ゴルーチンBが、まだ
racemalloc
によって適切に初期化されていない(競合検出の準備ができていない)tiny blockにアクセスしてしまう。
このシナリオでは、Race Detectorが誤った競合を報告したり、本来検出されるべき競合を見逃したりする可能性がありました。特に、tiny blockは複数のゴルーチン間で再利用されることが多いため、この問題は顕在化しやすかったと考えられます。
このコミットは、racemalloc
の呼び出しをプリエンプションが有効になる前に行うことで、このタイミングの問題を解決し、Race Detectorの正確性を向上させることを目的としています。
前提知識の解説
Go Runtime (Goランタイム)
Goプログラムの実行を管理するシステムです。メモリ管理(ガベージコレクション)、ゴルーチン(軽量スレッド)のスケジューリング、チャネル通信など、Go言語の並行処理モデルを支える基盤を提供します。C言語で書かれた部分とGo言語で書かれた部分があります。
Goroutines (ゴルーチン)
Go言語における軽量な並行実行単位です。OSスレッドよりもはるかに軽量で、数百万個のゴルーチンを同時に実行することも可能です。Goランタイムのスケジューラによって、OSスレッド上で多重化されて実行されます。
Preemption in Go (Goにおけるプリエンプション)
Goランタイムのスケジューラは、実行中のゴルーチンを強制的に中断(プリエンプト)し、別のゴルーチンにCPUを割り当てる機能を持っています。これにより、長時間実行されるゴルーチンが他のゴルーチンの実行を妨げることを防ぎ、公平なスケジューリングとシステムの応答性を保証します。プリエンプションは、関数呼び出しやループのバックエッジ(ループの終端から先頭に戻る部分)などで発生する可能性があります。
Race Detector (データ競合検出器)
Goに組み込まれているツールで、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みである場合に発生する「データ競合」を検出します。データ競合は、プログラムの動作を予測不能にし、デバッグを困難にするバグの一般的な原因です。Race Detectorは、実行時にメモリアクセスを監視し、競合の可能性を報告します。
racemalloc
GoのRace Detectorの一部として、メモリが割り当てられた際に呼び出される関数です。この関数は、新しく割り当てられたメモリ領域をRace Detectorが監視できるように初期化します。具体的には、そのメモリ領域が「クリーン」な状態であることをRace Detectorに伝え、その後のアクセスを監視対象とします。
Tiny Allocations (微小な割り当て)
Goのメモリマネージャは、割り当てられるオブジェクトのサイズに応じて異なる戦略を取ります。非常に小さなオブジェクト(例えば16バイト未満)は「tiny allocations」と呼ばれ、特別な方法で処理されます。これは、小さなオブジェクトが頻繁に割り当て・解放されるため、オーバーヘッドを最小限に抑えるための最適化です。複数のtiny allocationが同じメモリページ(tiny block)にパックされることがあります。
Memory Model (メモリモデル)
プログラミング言語におけるメモリモデルは、並行処理においてメモリへの読み書きがどのように順序付けられ、他のプロセッサやスレッドからどのように見えるかを定義するものです。データ競合は、メモリモデルの保証を破ることで発生し、予測不能な結果を招きます。
技術的詳細
このコミットが修正する問題は、Goランタイムのメモリ割り当て関数runtime·mallocgc
とRace Detectorのruntime·racemalloc
の相互作用におけるタイミングの問題です。
runtime·mallocgc
は、Goプログラムがメモリを要求した際に呼び出される主要なメモリ割り当て関数です。この関数は、要求されたサイズのメモリブロックをヒープから取得し、必要に応じてガベージコレクションをトリガーします。
GoのRace Detectorが有効な場合(raceenabled
がtrueの場合)、runtime·mallocgc
は割り当てられたメモリブロックに対してruntime·racemalloc
を呼び出す必要があります。runtime·racemalloc
の役割は、新しく割り当てられたメモリ領域がデータ競合検出の対象となるように、その領域の「レース状態」をクリアすることです。これは、そのメモリ領域に対する過去のアクセス履歴をリセットし、新しいアクセスが監視対象となるように準備する作業と考えることができます。
問題は、runtime·mallocgc
の内部で、runtime·racemalloc
の呼び出しと、ゴルーチンのプリエンプションを有効にする処理(g->stackguard0 = StackPreempt;
)の順序にありました。
元のコードでは、runtime·racemalloc
の呼び出しが、プリエンプションが有効になった後に行われていました。
// Old code snippet from malloc.goc
// ...
m->locks--;
if(m->locks == 0 && g->preempt) // restore the preemption request in case we've cleared it in newstack
g->stackguard0 = StackPreempt; // Preemption enabled here
// ...
if(raceenabled)
runtime·racemalloc(v, size); // racemalloc called after preemption enabled
この順序だと、以下のような競合シナリオが発生する可能性がありました。
- ゴルーチンAが
mallocgc
を実行し、メモリを割り当てる。 m->locks--
が実行され、ロックが解除される。g->stackguard0 = StackPreempt;
が実行され、現在のゴルーチンAがプリエンプト可能になる。- この直後、
runtime·racemalloc
が呼び出される前に、ゴルーチンAがGoスケジューラによってプリエンプトされる。 - 同じOSスレッド(M)上で、別のゴルーチンBが実行を開始する。
- ゴルーチンBが、ゴルーチンAが割り当てたばかりの、しかし
runtime·racemalloc
によってまだ「レース状態」がクリアされていない(つまり、Race Detectorが監視する準備ができていない)メモリブロックにアクセスしてしまう。 - このアクセスは、Race Detectorによって正しく検出されないか、あるいは誤った競合として報告される可能性があります。特に、tiny allocationsの場合、同じメモリブロックが複数のゴルーチン間で再利用される可能性が高いため、この問題はより顕著になります。
このコミットは、runtime·racemalloc
の呼び出しを、プリエンプションが有効になる前、つまりm->locks--
の直後、かつg->stackguard0 = StackPreempt;
の前に移動することで、このタイミングの問題を解決します。
// New code snippet from malloc.goc
// ...
m->locks--;
if(raceenabled) // racemalloc called before preemption enabled
runtime·racemalloc(v, size);
if(m->locks == 0 && g->preempt) // restore the preemption request in case we've cleared it in newstack
g->stackguard0 = StackPreempt; // Preemption enabled here
// ...
これにより、メモリブロックが割り当てられ、かつruntime·racemalloc
によってRace Detectorの監視対象として適切に初期化されてから、そのゴルーチンがプリエンプトされる可能性が生じるようになります。これにより、Race Detectorの正確性が保証され、特にtiny allocationsにおける誤検出や未検出が防止されます。
また、この変更を検証するために、src/pkg/runtime/race/testdata/mop_test.go
にTestNoRaceTinyAlloc
という新しいテストケースが追加されました。このテストは、複数のゴルーチンが同時に非常に小さなオブジェクトを割り当てるシナリオをシミュレートし、データ競合が検出されないことを確認することで、修正が正しく機能していることを検証します。
コアとなるコードの変更箇所
このコミットによるコードの変更は主に2つのファイルにあります。
-
src/pkg/runtime/malloc.goc
runtime·mallocgc
関数内のruntime·racemalloc
の呼び出し位置が変更されました。
-
src/pkg/runtime/race/testdata/mop_test.go
TestNoRaceTinyAlloc
という新しいテスト関数が追加されました。
src/pkg/runtime/malloc.goc
の変更点
--- a/src/pkg/runtime/malloc.goc
+++ b/src/pkg/runtime/malloc.goc
@@ -182,6 +182,8 @@ runtime·mallocgc(uintptr size, uintptr typ, uint32 flag)
m->mallocing = 0;
if(UseSpanType && !(flag & FlagNoScan) && typ != 0 && m->settype_bufsize == nelem(m->settype_buf))
runtime·settype_flush(m);
+ if(raceenabled)
+ runtime·racemalloc(v, size);
m->locks--;
if(m->locks == 0 && g->preempt) // restore the preemption request in case we've cleared it in newstack
g->stackguard0 = StackPreempt;
@@ -208,8 +210,6 @@ runtime·mallocgc(uintptr size, uintptr typ, uint32 flag)
if(!(flag & FlagNoInvokeGC) && mstats.heap_alloc >= mstats.next_gc)
runtime·gc(0);
- if(raceenabled)
- runtime·racemalloc(v, size);
return v;
}
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
@@ -1933,3 +1933,25 @@ func TestRaceMethodThunk4(t *testing.T) {
*(*int)(d.Base) = 42
<-done
}
++
++func TestNoRaceTinyAlloc(t *testing.T) {
++ const P = 4
++ const N = 1e6
++ var tinySink *byte
++ done := make(chan bool)
++ for p := 0; p < P; p++ {
++ go func() {
++ for i := 0; i < N; i++ {
++ var b byte
++ if b != 0 {
++ tinySink = &b // make it heap allocated
++ }
++ b = 42
++ }
++ done <- true
++ }()
++ }
++ for p := 0; p < P; p++ {
++ <-done
++ }
++}
コアとなるコードの解説
src/pkg/runtime/malloc.goc
の変更
この変更の核心は、runtime·mallocgc
関数内でのruntime·racemalloc
の呼び出し位置の移動です。
変更前:
runtime·racemalloc(v, size);
の呼び出しは、m->locks--
とg->stackguard0 = StackPreempt;
(プリエンプション有効化)の後にありました。
変更後:
runtime·racemalloc(v, size);
の呼び出しは、m->locks--
の直後、かつg->stackguard0 = StackPreempt;
(プリエンプション有効化)の前に移動されました。
この変更により、メモリブロックが割り当てられ、そのメモリブロックがRace Detectorによって「クリーン」な状態としてマークされてから、現在のゴルーチンがプリエンプトされる可能性が生じるようになります。これにより、プリエンプションによって別のゴルーチンが実行されたとしても、新しく割り当てられたメモリ領域がRace Detectorの監視対象として適切に準備されていることが保証され、データ競合の誤検出や見逃しが防止されます。
src/pkg/runtime/race/testdata/mop_test.go
の追加テスト
TestNoRaceTinyAlloc
は、この修正が正しく機能していることを検証するための新しいテストケースです。
const P = 4
: 4つのゴルーチンを並行して実行します。const N = 1e6
: 各ゴルーチンが100万回ループを実行します。var b byte
: 各ループで1バイトの変数を宣言します。これは非常に小さな割り当て(tiny allocation)の典型的な例です。if b != 0 { tinySink = &b }
: この部分は、コンパイラがb
をスタックに割り当てるのではなく、ヒープに割り当てるように強制するためのトリックです。tinySink
にアドレスを代入することで、b
がエスケープ解析によってヒープに割り当てられる可能性が高まります。これにより、実際にRace Detectorが監視するヒープ上のtiny allocationが生成されます。b = 42
: 割り当てられたメモリに値を書き込みます。
このテストは、複数のゴルーチンが同時に多数のtiny allocationsを行い、それらに書き込むというシナリオをシミュレートします。このシナリオでRace Detectorがデータ競合を報告しないことを確認することで、racemalloc
の呼び出しタイミングの修正が、tiny allocationsにおけるデータ競合の誤検出を防ぐのに成功していることを検証します。もし修正がなければ、このテストは競合を報告する可能性がありました。
関連リンク
- Go Issue #7224: https://github.com/golang/go/issues/7224
- このコミットが修正したGoのIssueです。詳細な議論や背景情報が記載されている可能性があります。
- Gerrit Change-Id: https://golang.org/cl/57730043
- GoのコードレビューシステムGerritにおけるこの変更のリンクです。レビューコメントや変更の経緯が確認できます。
参考にした情報源リンク
- Goの公式ドキュメント(特にメモリ管理、スケジューラ、Race Detectorに関するセクション)
- Goのソースコード(
src/runtime/malloc.go
、src/runtime/mgc.go
、src/runtime/proc.go
など) - GoのRace Detectorに関するブログ記事や解説
- GoのTiny Allocationsに関する技術記事
- Goのプリエンプションに関する技術記事
(注:具体的なURLは、Web検索で得られた情報に基づいて適宜追加してください。)
[インデックス 18369] ファイルの概要
このコミットは、Goランタイムにおけるメモリ割り当て、特に「tiny allocations(微小な割り当て)」に関するデータ競合検出器(Race Detector)の計測ロジックを調整するものです。具体的には、racemalloc
関数の呼び出しタイミングを変更することで、微小なメモリブロックが複数のゴルーチンによって共有される際に発生しうる競合状態の誤検出や未検出を防ぎます。
コミット
commit ce884036d2199ebec22e4f9200789a532a1225d1
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Tue Jan 28 22:34:32 2014 +0400
runtime: adjust malloc race instrumentation for tiny allocs
Tiny alloc memory block is shared by different goroutines running on the same thread.
We call racemalloc after enabling preemption in mallocgc,
as the result another goroutine can act on not yet race-cleared tiny block.
Call racemalloc before enabling preemption.
Fixes #7224.
LGTM=dave
R=golang-codereviews, dave
CC=golang-codereviews
https://golang.org/cl/57730043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/ce884036d2199ebec22e4f9200789a532a1225d1
元コミット内容
このコミットは、Goランタイムのメモリ割り当て処理において、データ競合検出器(Race Detector)のracemalloc
関数の呼び出しタイミングを修正します。
問題は、微小なメモリ割り当て(tiny allocs)の場合、同じスレッド上で実行されている異なるゴルーチン間でメモリブロックが共有される可能性がある点にありました。
これまでの実装では、mallocgc
関数内でプリエンプション(横取り)が有効になった後にracemalloc
が呼び出されていました。このタイミングだと、racemalloc
がまだ競合検出のためのクリア処理を終えていない微小ブロックに対して、別のゴルーチンがアクセスしてしまう可能性があり、これがデータ競合を引き起こす、あるいは誤った競合検出につながる可能性がありました。
このコミットでは、racemalloc
の呼び出しをプリエンプションが有効になる前に行うように変更することで、この問題を解決しています。
これはGoのIssue #7224を修正するものです。
変更の背景
Goのランタイムは、効率的なメモリ管理のために様々な最適化を行っています。その一つが「tiny allocations」と呼ばれる、非常に小さなオブジェクト(例えば16バイト未満)の割り当てに対する特殊なハンドリングです。これらの小さなオブジェクトは、通常、専用の小さなメモリブロック(tiny block)から割り当てられます。
問題の核心は、このtiny blockが、同じOSスレッド(M: マシン)上で実行されている複数のGoルーチン(G)間で共有される可能性があるという点にありました。Goのスケジューラは、M上でGをプリエンプト(横取り)し、別のGを実行させることができます。
GoのRace Detectorは、メモリへのアクセス競合を検出するための強力なツールです。メモリが割り当てられた際、racemalloc
関数が呼び出され、そのメモリ領域が競合検出の対象として適切に初期化(クリア)されます。
これまでのmallocgc
の実装では、racemalloc
の呼び出しが、プリエンプションが有効になる(g->stackguard0 = StackPreempt;
が設定される)後に行われていました。この順序だと、以下のような競合状態が発生する可能性がありました。
- ゴルーチンAが
mallocgc
を呼び出し、tiny blockを割り当てる。 mallocgc
内でプリエンプションが有効になる。racemalloc
が呼び出される前に、ゴルーチンAがプリエンプトされる。- 同じOSスレッド上で、別のゴルーチンBが実行される。
- ゴルーチンBが、まだ
racemalloc
によって適切に初期化されていない(競合検出の準備ができていない)tiny blockにアクセスしてしまう。
このシナリオでは、Race Detectorが誤った競合を報告したり、本来検出されるべき競合を見逃したりする可能性がありました。特に、tiny blockは複数のゴルーチン間で再利用されることが多いため、この問題は顕在化しやすかったと考えられます。
このコミットは、racemalloc
の呼び出しをプリエンプションが有効になる前に行うことで、このタイミングの問題を解決し、Race Detectorの正確性を向上させることを目的としています。
前提知識の解説
Go Runtime (Goランタイム)
Goプログラムの実行を管理するシステムです。メモリ管理(ガベージコレクション)、ゴルーチン(軽量スレッド)のスケジューリング、チャネル通信など、Go言語の並行処理モデルを支える基盤を提供します。C言語で書かれた部分とGo言語で書かれた部分があります。
Goroutines (ゴルーチン)
Go言語における軽量な並行実行単位です。OSスレッドよりもはるかに軽量で、数百万個のゴルーチンを同時に実行することも可能です。Goランタイムのスケジューラによって、OSスレッド上で多重化されて実行されます。
Preemption in Go (Goにおけるプリエンプション)
Goランタイムのスケジューラは、実行中のゴルーチンを強制的に中断(プリエンプト)し、別のゴルーチンにCPUを割り当てる機能を持っています。これにより、長時間実行されるゴルーチンが他のゴルーチンの実行を妨げることを防ぎ、公平なスケジューリングとシステムの応答性を保証します。プリエンプションは、関数呼び出しやループのバックエッジ(ループの終端から先頭に戻る部分)などで発生する可能性があります。
Race Detector (データ競合検出器)
Goに組み込まれているツールで、複数のゴルーチンが同時に同じメモリ位置にアクセスし、少なくとも1つのアクセスが書き込みである場合に発生する「データ競合」を検出します。データ競合は、プログラムの動作を予測不能にし、デバッグを困難にするバグの一般的な原因です。Race Detectorは、実行時にメモリアクセスを監視し、競合の可能性を報告します。
racemalloc
GoのRace Detectorの一部として、メモリが割り当てられた際に呼び出される関数です。この関数は、新しく割り当てられたメモリ領域をRace Detectorが監視できるように初期化します。具体的には、そのメモリ領域が「クリーン」な状態であることをRace Detectorに伝え、その後のアクセスを監視対象とします。
Tiny Allocations (微小な割り当て)
Goのメモリマネージャは、割り当てられるオブジェクトのサイズに応じて異なる戦略を取ります。非常に小さなオブジェクト(例えば16バイト未満)は「tiny allocations」と呼ばれ、特別な方法で処理されます。これは、小さなオブジェクトが頻繁に割り当て・解放されるため、オーバーヘッドを最小限に抑えるための最適化です。複数のtiny allocationが同じメモリページ(tiny block)にパックされることがあります。
Memory Model (メモリモデル)
プログラミング言語におけるメモリモデルは、並行処理においてメモリへの読み書きがどのように順序付けられ、他のプロセッサやスレッドからどのように見えるかを定義するものです。データ競合は、メモリモデルの保証を破ることで発生し、予測不能な結果を招きます。
技術的詳細
このコミットが修正する問題は、Goランタイムのメモリ割り当て関数runtime·mallocgc
とRace Detectorのruntime·racemalloc
の相互作用におけるタイミングの問題です。
runtime·mallocgc
は、Goプログラムがメモリを要求した際に呼び出される主要なメモリ割り当て関数です。この関数は、要求されたサイズのメモリブロックをヒープから取得し、必要に応じてガベージコレクションをトリガーします。
GoのRace Detectorが有効な場合(raceenabled
がtrueの場合)、runtime·mallocgc
は割り当てられたメモリブロックに対してruntime·racemalloc
を呼び出す必要があります。runtime·racemalloc
の役割は、新しく割り当てられたメモリ領域がデータ競合検出の対象となるように、その領域の「レース状態」をクリアすることです。これは、そのメモリ領域に対する過去のアクセス履歴をリセットし、新しいアクセスが監視対象となるように準備する作業と考えることができます。
問題は、runtime·mallocgc
の内部で、runtime·racemalloc
の呼び出しと、ゴルーチンのプリエンプションを有効にする処理(g->stackguard0 = StackPreempt;
)の順序にありました。
元のコードでは、runtime·racemalloc
の呼び出しが、プリエンプションが有効になった後に行われていました。
// Old code snippet from malloc.goc
// ...
m->locks--;
if(m->locks == 0 && g->preempt) // restore the preemption request in case we've cleared it in newstack
g->stackguard0 = StackPreempt; // Preemption enabled here
// ...
if(raceenabled)
runtime·racemalloc(v, size); // racemalloc called after preemption enabled
この順序だと、以下のような競合シナリオが発生する可能性がありました。
- ゴルーチンAが
mallocgc
を実行し、メモリを割り当てる。 m->locks--
が実行され、ロックが解除される。g->stackguard0 = StackPreempt;
が実行され、現在のゴルーチンAがプリエンプト可能になる。- この直後、
runtime·racemalloc
が呼び出される前に、ゴルーチンAがGoスケジューラによってプリエンプトされる。 - 同じOSスレッド(M)上で、別のゴルーチンBが実行を開始する。
- ゴルーチンBが、ゴルーチンAが割り当てたばかりの、しかし
runtime·racemalloc
によってまだ「レース状態」がクリアされていない(つまり、Race Detectorが監視する準備ができていない)メモリブロックにアクセスしてしまう。 - このアクセスは、Race Detectorによって正しく検出されないか、あるいは誤った競合として報告される可能性があります。特に、tiny allocationsの場合、同じメモリブロックが複数のゴルーチン間で再利用される可能性が高いため、この問題はより顕著になります。
このコミットは、runtime·racemalloc
の呼び出しを、プリエンプションが有効になる前、つまりm->locks--
の直後、かつg->stackguard0 = StackPreempt;
の前に移動することで、このタイミングの問題を解決します。
// New code snippet from malloc.goc
// ...
m->locks--;
if(raceenabled) // racemalloc called before preemption enabled
runtime·racemalloc(v, size);
if(m->locks == 0 && g->preempt) // restore the preemption request in case we've cleared it in newstack
g->stackguard0 = StackPreempt; // Preemption enabled here
// ...
これにより、メモリブロックが割り当てられ、かつruntime·racemalloc
によってRace Detectorの監視対象として適切に初期化されてから、そのゴルーチンがプリエンプトされる可能性が生じるようになります。これにより、Race Detectorの正確性が保証され、特にtiny allocationsにおける誤検出や未検出が防止されます。
また、この変更を検証するために、src/pkg/runtime/race/testdata/mop_test.go
にTestNoRaceTinyAlloc
という新しいテストケースが追加されました。このテストは、複数のゴルーチンが同時に非常に小さなオブジェクトを割り当てるシナリオをシミュレートし、データ競合が検出されないことを確認することで、修正が正しく機能していることを検証します。
コアとなるコードの変更箇所
このコミットによるコードの変更は主に2つのファイルにあります。
-
src/pkg/runtime/malloc.goc
runtime·mallocgc
関数内のruntime·racemalloc
の呼び出し位置が変更されました。
-
src/pkg/runtime/race/testdata/mop_test.go
TestNoRaceTinyAlloc
という新しいテスト関数が追加されました。
src/pkg/runtime/malloc.goc
の変更点
--- a/src/pkg/runtime/malloc.goc
+++ b/src/pkg/runtime/malloc.goc
@@ -182,6 +182,8 @@ runtime·mallocgc(uintptr size, uintptr typ, uint32 flag)
m->mallocing = 0;
if(UseSpanType && !(flag & FlagNoScan) && typ != 0 && m->settype_bufsize == nelem(m->settype_buf))
runtime·settype_flush(m);
+ if(raceenabled)
+ runtime·racemalloc(v, size);
m->locks--;
if(m->locks == 0 && g->preempt) // restore the preemption request in case we've cleared it in newstack
g->stackguard0 = StackPreempt;
@@ -208,8 +210,6 @@ runtime·mallocgc(uintptr size, uintptr typ, uint32 flag)
if(!(flag & FlagNoInvokeGC) && mstats.heap_alloc >= mstats.next_gc)
runtime·gc(0);
- if(raceenabled)
- runtime·racemalloc(v, size);
return v;
}
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
@@ -1933,3 +1933,25 @@ func TestRaceMethodThunk4(t *testing.T) {
*(*int)(d.Base) = 42
<-done
}
++
++func TestNoRaceTinyAlloc(t *testing.T) {
++ const P = 4
++ const N = 1e6
++ var tinySink *byte
++ done := make(chan bool)
++ for p := 0; p < P; p++ {
++ go func() {
++ for i := 0; i < N; i++ {
++ var b byte
++ if b != 0 {
++ tinySink = &b // make it heap allocated
++ }
++ b = 42
++ }
++ done <- true
++ }()
++ }
++ for p := 0; p < P; p++ {
++ <-done
++ }
++}
コアとなるコードの解説
src/pkg/runtime/malloc.goc
の変更
この変更の核心は、runtime·mallocgc
関数内でのruntime·racemalloc
の呼び出し位置の移動です。
変更前:
runtime·racemalloc(v, size);
の呼び出しは、m->locks--
とg->stackguard0 = StackPreempt;
(プリエンプション有効化)の後にありました。
変更後:
runtime·racemalloc(v, size);
の呼び出しは、m->locks--
の直後、かつg->stackguard0 = StackPreempt;
(プリエンプション有効化)の前に移動されました。
この変更により、メモリブロックが割り当てられ、そのメモリブロックがRace Detectorによって「クリーン」な状態としてマークされてから、現在のゴルーチンがプリエンプトされる可能性が生じるようになります。これにより、プリエンプションによって別のゴルーチンが実行されたとしても、新しく割り当てられたメモリ領域がRace Detectorの監視対象として適切に準備されていることが保証され、データ競合の誤検出や見逃しが防止されます。
src/pkg/runtime/race/testdata/mop_test.go
の追加テスト
TestNoRaceTinyAlloc
は、この修正が正しく機能していることを検証するための新しいテストケースです。
const P = 4
: 4つのゴルーチンを並行して実行します。const N = 1e6
: 各ゴルーチンが100万回ループを実行します。var b byte
: 各ループで1バイトの変数を宣言します。これは非常に小さな割り当て(tiny allocation)の典型的な例です。if b != 0 { tinySink = &b }
: この部分は、コンパイラがb
をスタックに割り当てるのではなく、ヒープに割り当てるように強制するためのトリックです。tinySink
にアドレスを代入することで、b
がエスケープ解析によってヒープに割り当てられる可能性が高まります。これにより、実際にRace Detectorが監視するヒープ上のtiny allocationが生成されます。b = 42
: 割り当てられたメモリに値を書き込みます。
このテストは、複数のゴルーチンが同時に多数のtiny allocationsを行い、それらに書き込むというシナリオをシミュレートします。このシナリオでRace Detectorがデータ競合を報告しないことを確認することで、racemalloc
の呼び出しタイミングの修正が、tiny allocationsにおけるデータ競合の誤検出を防ぐのに成功していることを検証します。もし修正がなければ、このテストは競合を報告する可能性がありました。
関連リンク
- Go Issue #7224: https://github.com/golang/go/issues/7224
- このコミットが修正したGoのIssueです。詳細な議論や背景情報が記載されている可能性があります。
- Gerrit Change-Id: https://golang.org/cl/57730043
- GoのコードレビューシステムGerritにおけるこの変更のリンクです。レビューコメントや変更の経緯が確認できます。
参考にした情報源リンク
- Goの公式ドキュメント(特にメモリ管理、スケジューラ、Race Detectorに関するセクション)
- Goのソースコード(
src/runtime/malloc.go
、src/runtime/mgc.go
、src/runtime/proc.go
など) - GoのRace Detectorに関するブログ記事や解説
- GoのTiny Allocationsに関する技術記事
- Goのプリエンプションに関する技術記事
- Go runtime: adjust malloc race instrumentation for tiny allocs - Google Search: https://www.google.com/search?q=Go+runtime%3A+adjust+malloc+race+instrumentation+for+tiny+allocs
- Go runtime tiny allocations race detector preemption - Google Search: https://www.google.com/search?q=Go+runtime+tiny+allocations+race+detector+preemption