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

[インデックス 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;が設定される)後に行われていました。この順序だと、以下のような競合状態が発生する可能性がありました。

  1. ゴルーチンAがmallocgcを呼び出し、tiny blockを割り当てる。
  2. mallocgc内でプリエンプションが有効になる。
  3. racemallocが呼び出される前に、ゴルーチンAがプリエンプトされる。
  4. 同じOSスレッド上で、別のゴルーチンBが実行される。
  5. ゴルーチン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

この順序だと、以下のような競合シナリオが発生する可能性がありました。

  1. ゴルーチンAがmallocgcを実行し、メモリを割り当てる。
  2. m->locks--が実行され、ロックが解除される。
  3. g->stackguard0 = StackPreempt;が実行され、現在のゴルーチンAがプリエンプト可能になる。
  4. この直後、runtime·racemallocが呼び出される前に、ゴルーチンAがGoスケジューラによってプリエンプトされる。
  5. 同じOSスレッド(M)上で、別のゴルーチンBが実行を開始する。
  6. ゴルーチンBが、ゴルーチンAが割り当てたばかりの、しかしruntime·racemallocによってまだ「レース状態」がクリアされていない(つまり、Race Detectorが監視する準備ができていない)メモリブロックにアクセスしてしまう。
  7. このアクセスは、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.goTestNoRaceTinyAllocという新しいテストケースが追加されました。このテストは、複数のゴルーチンが同時に非常に小さなオブジェクトを割り当てるシナリオをシミュレートし、データ競合が検出されないことを確認することで、修正が正しく機能していることを検証します。

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

このコミットによるコードの変更は主に2つのファイルにあります。

  1. src/pkg/runtime/malloc.goc

    • runtime·mallocgc関数内のruntime·racemallocの呼び出し位置が変更されました。
  2. 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.gosrc/runtime/mgc.gosrc/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;が設定される)後に行われていました。この順序だと、以下のような競合状態が発生する可能性がありました。

  1. ゴルーチンAがmallocgcを呼び出し、tiny blockを割り当てる。
  2. mallocgc内でプリエンプションが有効になる。
  3. racemallocが呼び出される前に、ゴルーチンAがプリエンプトされる。
  4. 同じOSスレッド上で、別のゴルーチンBが実行される。
  5. ゴルーチン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

この順序だと、以下のような競合シナリオが発生する可能性がありました。

  1. ゴルーチンAがmallocgcを実行し、メモリを割り当てる。
  2. m->locks--が実行され、ロックが解除される。
  3. g->stackguard0 = StackPreempt;が実行され、現在のゴルーチンAがプリエンプト可能になる。
  4. この直後、runtime·racemallocが呼び出される前に、ゴルーチンAがGoスケジューラによってプリエンプトされる。
  5. 同じOSスレッド(M)上で、別のゴルーチンBが実行を開始する。
  6. ゴルーチンBが、ゴルーチンAが割り当てたばかりの、しかしruntime·racemallocによってまだ「レース状態」がクリアされていない(つまり、Race Detectorが監視する準備ができていない)メモリブロックにアクセスしてしまう。
  7. このアクセスは、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.goTestNoRaceTinyAllocという新しいテストケースが追加されました。このテストは、複数のゴルーチンが同時に非常に小さなオブジェクトを割り当てるシナリオをシミュレートし、データ競合が検出されないことを確認することで、修正が正しく機能していることを検証します。

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

このコミットによるコードの変更は主に2つのファイルにあります。

  1. src/pkg/runtime/malloc.goc

    • runtime·mallocgc関数内のruntime·racemallocの呼び出し位置が変更されました。
  2. 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におけるこの変更のリンクです。レビューコメントや変更の経緯が確認できます。

参考にした情報源リンク