[インデックス 13429] ファイルの概要
このコミットは、Go言語のテストスイート内の test/closure.go
ファイルに対する変更です。具体的には、テストの実行環境を制御し、特定のテストが GOMAXPROCS
の設定に依存しないように安定させることを目的としています。
コミット
- コミットハッシュ:
91e56e6486a24a9e8cced7197df7cef6cba6da1a
- 作者: Dmitriy Vyukov dvyukov@google.com
- 日付: Sun Jul 1 21:59:50 2012 +0400
- コミットメッセージ:
test: enforce 1 proc in the test otherwise it fails spuriously with "newfunc allocated unexpectedly" message when run with GOMAXPROCS>1 (other goroutine allocates). R=golang-dev, dsymonds CC=golang-dev https://golang.org/cl/6347056
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/91e56e6486a24a9e8cced7197df7cef6cba6da1a
元コミット内容
test: enforce 1 proc in the test
otherwise it fails spuriously with "newfunc allocated unexpectedly" message
when run with GOMAXPROCS>1 (other goroutine allocates).
R=golang-dev, dsymonds
CC=golang-dev
https://golang.org/cl/6347056
変更の背景
このコミットの背景には、Go言語のテスト実行における非決定的な振る舞いがありました。具体的には、test/closure.go
内のテストが、GOMAXPROCS
環境変数の値が1より大きい場合に「newfunc allocated unexpectedly」というメッセージと共に誤って失敗することがありました。
この問題は、テストが特定の条件下(おそらく、特定の関数がメモリを割り当てる回数やタイミング)でしか成功しないように設計されていたためと考えられます。しかし、GOMAXPROCS
が1より大きい場合、Goランタイムは複数のOSスレッドを使用してゴルーチンを並行して実行しようとします。これにより、テストが想定していないタイミングで他のゴルーチンがメモリ割り当てを行う可能性があり、テストの期待値と実際の動作が乖離し、テストが不安定になる(spuriously fails)という問題が発生していました。
このコミットは、テストの安定性を確保するために、テスト実行時に GOMAXPROCS
を明示的に1に設定することで、この非決定的な振る舞いを排除することを目的としています。これにより、テストは常に単一のOSスレッド上で実行されるようになり、他のゴルーチンによる予期せぬメモリ割り当ての影響を受けなくなります。
前提知識の解説
GOMAXPROCS
GOMAXPROCS
は、Goランタイムが同時にGoコードを実行するために使用できるOSスレッドの最大数を制御する環境変数、または runtime
パッケージの関数です。
- ゴルーチンとOSスレッド: Goプログラムは、Goランタイムによって管理される軽量なユーザーレベルスレッドであるゴルーチンを使用します。これらのゴルーチンは、CPUコア上で実行するために実際のOSスレッドにスケジュールされる必要があります。
GOMAXPROCS
は、Goランタイムがゴルーチンを並行して実行するために使用できるOSスレッドの数を制限します。 - デフォルト値:
- Go 1.5より前は、
GOMAXPROCS
のデフォルト値は1でした。 - Go 1.5以降は、
GOMAXPROCS
のデフォルト値は、プログラムが利用できる論理CPUコアの数になりました。このデフォルトは、ほとんどのアプリケーションで最適なパフォーマンスを提供します。
- Go 1.5より前は、
- パフォーマンスへの影響:
- 並行性と並列性:
GOMAXPROCS
は、並行性(多くのタスクを管理すること)と並列性(複数のコアでタスクを同時に実行すること)のバランスを取るのに役立ちます。 - 高すぎる設定: 利用可能なCPUコア数よりも
GOMAXPROCS
を高く設定しても、Goプログラムが高速になるわけではなく、パフォーマンスが低下する可能性があります。これは、より多くのOSスレッドが限られたCPU時間を奪い合うため、コンテキストスイッチが増加する可能性があるためです。 - 低すぎる設定: 利用可能なコア数よりも
GOMAXPROCS
を低く設定すると、利用可能なCPUリソースを十分に活用できず、プログラムのパフォーマンスに影響を与える可能性があります。
- 並行性と並列性:
- 設定方法:
- 環境変数: プログラムを実行する前に
GOMAXPROCS
環境変数を設定します。 - プログラム内: Goプログラム内で
runtime.GOMAXPROCS()
を呼び出します。
- 環境変数: プログラムを実行する前に
Goのゴルーチンとスケジューリング
Goのゴルーチンスケジューリングは、Goランタイムによって管理され、M:Nスケジューリング技術を採用しています。これは、多数のゴルーチン(軽量なユーザー空間スレッド)を少数のOSスレッドに効率的に多重化するものです。このシステムは、G-M-Pモデルで説明されることが多いです。
- G (Goroutine): 軽量で独立して実行される関数を表します。ゴルーチンは、従来のOSスレッドよりも作成と管理がはるかに安価です。
- M (Machine): Goコードを実行できるOSスレッドを表します。Goランタイムは必要に応じてOSスレッドを作成し、MはユーザーのGoコード、ランタイムコードを実行したり、アイドル状態になったりすることができます。
- P (Processor): ユーザーのGoコードを実行するために必要な論理プロセッサまたはコンテキストを表します。
GOMAXPROCS
と同じ数のPが存在し、デフォルトでは利用可能な論理CPUの数に設定されます。各Pは、実行準備ができたゴルーチンのローカル実行キューを持っています。
スケジューリングの仕組みは以下の通りです。
- 多重化: Goスケジューラの役割は、ゴルーチン(G)をプロセッサ(P)とOSスレッド(M)に割り当てることです。Pはディスパッチャとして機能し、ローカルキューからゴルーチンをMに割り当てます。
- 実行キュー: 各Pは、割り当てられたゴルーチンのローカル実行キュー(LRQ)を持っています。また、まだLRQに割り当てられていないゴルーチンのグローバル実行キュー(GRQ)もあります。スケジューラは定期的にGRQからLRQにゴルーチンを移動させます。
- 協調的スケジューリング: Goスケジューラは協調的に動作します。つまり、ゴルーチンは特定のチェックポイントで自発的に制御を譲ります。これは、関数呼び出し、ガベージコレクション、ネットワーク操作、チャネル操作中に発生する可能性があります。
- ワークスティーリング: ワークロードのバランスを取るために、Pがローカルキュー内のゴルーチンを使い果たした場合、別のPのローカルキューまたはグローバル実行キューからゴルーチンを「盗む」ことができます。
- ブロッキングシステムコール: ゴルーチンがブロッキングシステムコール(例: ファイルからの読み取り)を行うと、そのゴルーチンが実行されているMがブロックされる可能性があります。Goランタイムは、ブロックされたMからPを切り離し、新しいまたは既存のアイドル状態のMにPをアタッチすることで、他のゴルーチンが中断することなく実行を継続できるようにします。
- プリエンプション: 歴史的にはより協調的でしたが、現代のGoスケジューラはプリエンプションも組み込んでいます。ゴルーチンが長時間(例: 10ms以上)実行された場合、スケジューラはそれをプリエンプトし、グローバル実行キューに移動させて、他のゴルーチンに実行機会を与えることができます。
「newfunc allocated unexpectedly」メッセージ
「newfunc allocated unexpectedly」というメッセージは、通常、特定の関数(この場合は newfunc
がプレースホルダー)が予期せず多くのメモリ割り当てを行っていることを示唆しています。これはパフォーマンスに影響を与える可能性があります。このメッセージは、主にプロファイリングツールを通じて特定されます。
このメッセージは、テストが特定のメモリ割り当てパターンや回数を期待している場合に、実際の実行でそれが満たされないときに発生することがあります。特に、並行処理が行われる環境(GOMAXPROCS > 1
の場合)では、他のゴルーチンが予期せぬタイミングでメモリ割り当てを行うことで、テストの期待値が崩れる可能性があります。
この問題を診断するには、Goの組み込みプロファイリングツール、特に pprof
を使用して、これらの予期せぬ割り当てが発生しているコードの正確な場所を特定します。
技術的詳細
このコミットは、test/closure.go
というテストファイルにおいて、runtime.GOMAXPROCS(1)
を呼び出すことで、テストの実行環境を単一のプロセッサ(OSスレッド)に制限しています。
test/closure.go
は、Goのクロージャの動作を検証するテストであると推測されます。このようなテストでは、メモリ割り当てのタイミングや回数がテストの成功条件に影響を与える場合があります。例えば、テストが特定のクロージャが特定の回数だけメモリを割り当てることを期待している場合、他のゴルーチンが並行して実行され、予期せぬメモリ割り当てを行うと、テストが失敗する可能性があります。
GOMAXPROCS
が1より大きい場合、Goランタイムは複数のOSスレッドを利用してゴルーチンを並列に実行しようとします。これにより、テスト内の go f()
のような並行して実行されるゴルーチンが、テストのメインロジックとは独立してメモリ割り当てを行う可能性があります。コミットメッセージにある「other goroutine allocates」という記述は、この状況を指しています。
runtime.GOMAXPROCS(1)
をテストの main
関数内で呼び出すことで、このテストの実行中はGoランタイムが使用するOSスレッドの数を1に制限します。これにより、テストは実質的に単一スレッド環境で実行されることになり、他のゴルーチンによる並行なメモリ割り当てがテストの期待値に影響を与えることを防ぎます。結果として、テストの非決定的な失敗(spuriously fails)が解消され、テストの安定性が向上します。
この変更は、テストの正確性を保証するために、テスト環境を厳密に制御する一般的な手法です。特に、並行処理の挙動に敏感なテストにおいては、このような環境制御が不可欠となる場合があります。
コアとなるコードの変更箇所
--- a/test/closure.go
+++ b/test/closure.go
@@ -81,6 +81,7 @@ func h() {
func newfunc() func(int) int { return func(x int) int { return x } }\n
func main() {\n+\truntime.GOMAXPROCS(1)\n \tvar fail bool\n \n \tgo f()\n```
## コアとなるコードの解説
変更は `test/closure.go` ファイルの `main` 関数内の一行です。
```go
func main() {
runtime.GOMAXPROCS(1) // 追加された行
var fail bool
go f()
// ...
}
追加された runtime.GOMAXPROCS(1)
は、Goの runtime
パッケージが提供する関数で、GoランタイムがGoコードを実行するために使用できるOSスレッドの最大数を1に設定します。
この行が main
関数の冒頭に追加されたことで、test/closure.go
のテストが実行される際には、Goランタイムは常に単一のOSスレッドのみを使用するようになります。これにより、テスト内で起動される他のゴルーチン(例: go f()
)が、テストの期待値に影響を与えるような予期せぬメモリ割り当てを並行して行う可能性が排除されます。
この変更は、テストの実行環境を標準化し、GOMAXPROCS
の設定に依存しない安定したテスト結果を保証するために不可欠でした。
関連リンク
参考にした情報源リンク
- Go GOMAXPROCS explanation - programmerscareer.com
- Go GOMAXPROCS explanation - oreilly.com
- Go GOMAXPROCS explanation - medium.com
- Go goroutine scheduling - kodekloud.com
- Go goroutine scheduling - go.dev
- Go goroutine scheduling - ardanlabs.com
- Go "newfunc allocated unexpectedly" - stackoverflow.com (General information about unexpected allocations and pprof)