[インデックス 13433] ファイルの概要
このコミットは、Go言語のreflect
パッケージ内のテストファイルsrc/pkg/reflect/all_test.go
におけるメモリ割り当てテストの閾値を調整するものです。具体的には、GOMAXPROCS
が1より大きい環境で発生するテストの不安定性(flakiness)を解消するために、許容されるメモリ割り当て(mallocs)の最大値を増やしています。
コミット
- コミットハッシュ:
21de1ab359f198fff172f5e2bbaa29ab955a4688
- 作者: Dmitriy Vyukov dvyukov@google.com
- コミット日時: 2012年7月2日 月曜日 20:55:08 +0400
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/21de1ab359f198fff172f5e2bbaa29ab955a4688
元コミット内容
reflect: set GOMAXPROCS=1 in the malloc test
Occasionally I see:
--- FAIL: TestAllocations-15 (0.00 seconds)
all_test.go:1575: 6 mallocs after 100 iterations
Tested:
$ go test -cpu=1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20 reflect
R=golang-dev, dsymonds, r
CC=golang-dev
https://golang.org/cl/6354063
変更の背景
このコミットの主な背景は、Go言語のreflect
パッケージにおけるメモリ割り当てテスト(TestAllocations
)が、特定の条件下で不安定に失敗するという問題でした。コミットメッセージに記載されているように、GOMAXPROCS
環境変数を1より大きい値に設定してテストを実行すると、時折"6 mallocs after 100 iterations"
というエラーでテストが失敗していました。
GOMAXPROCS
はGoランタイムが同時に実行できるOSスレッドの最大数を制御します。デフォルトではCPUコア数に設定されます。この値が1より大きい場合、Goランタイムは複数のOSスレッドを利用して並行処理を行います。
問題のテストnoAlloc
関数は、特定の操作が余分なメモリ割り当てを行わないことを検証することを目的としていました。しかし、GOMAXPROCS > 1
の環境では、Goランタイムが新しいOSスレッドを起動する際に、そのスレッド自身の初期化のために少量のメモリ割り当て(コミットメッセージによると約6回のmalloc)が発生することが判明しました。この割り当ては、テスト対象の関数自体が行うものではなく、ランタイムの並行処理設定に起因するものであり、テストの意図とは異なるものでした。
元のテストでは、許容されるメモリ割り当ての最大値が5に設定されていたため、このランタイムによる追加の6回の割り当てが発生すると、テストが失敗してしまっていました。このため、テストが環境によって不安定になる「flaky test」の状態に陥っていました。このコミットは、この既知の、しかしテストの意図とは異なる割り当てを許容することで、テストの安定性を向上させることを目的としています。
前提知識の解説
Go言語のreflect
パッケージ
reflect
パッケージは、Goプログラムの実行時に型情報を検査・操作するための機能を提供します。これにより、プログラムは自身の構造を調べたり、動的に値を操作したりすることが可能になります。例えば、構造体のフィールド名や型を取得したり、インターフェースの値の具体的な型を調べたりする際に使用されます。このコミットで変更されたall_test.go
は、reflect
パッケージの内部的なテストコードの一部です。
GOMAXPROCS
環境変数
GOMAXPROCS
はGoランタイムが同時に実行できるOSスレッドの最大数を設定する環境変数です。
GOMAXPROCS=1
の場合、Goランタイムは同時に1つのOSスレッドしか使用しません。これにより、並行処理が制限され、シングルスレッドに近い動作になります。GOMAXPROCS > 1
の場合、Goランタイムは指定された数のOSスレッドを同時に利用し、複数のゴルーチンを並行して実行できるようになります。これにより、マルチコアCPUの性能を最大限に活用できます。 このコミットの文脈では、GOMAXPROCS > 1
のときにGoランタイムが追加のスレッドを起動し、それに伴う初期メモリ割り当てが発生することが問題の原因となっています。
runtime.ReadMemStats
関数
runtime.ReadMemStats
関数は、Goプログラムの現在のメモリ統計情報(ヒープの使用量、ガベージコレクションの回数、メモリ割り当ての総数など)をruntime.MemStats
構造体に読み込みます。このコミットのテストでは、この関数を使ってテスト対象のコードが実行される前後のメモリ割り当て回数(MemStats.Mallocs
)を比較し、余分な割り当てが発生していないかを確認しています。
testing
パッケージ
Go言語の標準ライブラリに含まれるtesting
パッケージは、ユニットテストやベンチマークテストを記述するためのフレームワークを提供します。テスト関数はTest
で始まる名前を持ち、*testing.T
型の引数を取ります。この引数を通じて、テストの失敗を報告したり、ログを出力したりします。
メモリ割り当て (mallocs)
mallocs
は、プログラムが実行時に動的にメモリを確保する操作(C言語のmalloc
に由来)を指します。Go言語では、通常、make
やnew
キーワード、あるいは複合リテラルなどによってヒープメモリが割り当てられます。Goランタイムはガベージコレクタによってメモリ管理を行いますが、テストの文脈では、特定の処理が不必要に多くのメモリ割り当てを行っていないか(パフォーマンスや効率の観点から)を検証することが重要になります。
テストの不安定性 (Flakiness)
「Flaky test」とは、同じコードに対して同じテストを複数回実行しても、成功したり失敗したりと結果が不安定なテストを指します。このようなテストは、並行処理のタイミング、環境変数、外部リソースの可用性、あるいはランダムな要素など、非決定的な要因によって引き起こされることが多いです。このコミットのケースでは、GOMAXPROCS
の設定という環境要因がテストの不安定性を引き起こしていました。
技術的詳細
変更が行われたsrc/pkg/reflect/all_test.go
内のnoAlloc
関数は、特定の関数f
が実行中にメモリ割り当てを行わないことを検証するためのヘルパー関数です。
この関数のロジックは以下の通りです。
- テスト対象の関数
f
を実行する前に、runtime.ReadMemStats
を呼び出して現在のメモリ統計情報(特にMallocs
、つまりメモリ割り当ての総回数)を記録します。 - テスト対象の関数
f
をn
回実行します。 - 関数実行後に再度
runtime.ReadMemStats
を呼び出し、メモリ割り当ての総回数を取得します。 - 前後の
Mallocs
の差分を計算し、テスト対象の関数が実行中にどれだけのメモリ割り当てを行ったかを算出します。 - この差分(
mallocs
変数)が許容される閾値を超えていないかをチェックします。
元のコードでは、この閾値が5
に設定されていました (if mallocs > 5
)。しかし、GOMAXPROCS > 1
の場合、Goランタイムは並行処理のために新しいOSスレッドを起動することがあります。コミットメッセージと追加されたコメントによると、この新しいスレッドの起動自体が約6回のメモリ割り当てを伴うことが判明しました。
// A few allocs may happen in the testing package when GOMAXPROCS > 1, so don't
// require zero mallocs.
// A new thread, one of which will be created if GOMAXPROCS>1, does 6 allocations.
runtime.ReadMemStats(memstats)
mallocs := memstats.Mallocs - oldmallocs
if mallocs > 10 { // 変更箇所: 5 -> 10
t.Fatalf("%d mallocs after %d iterations", mallocs, n)
}
この「新しいスレッドによる6回の割り当て」は、テスト対象の関数f
の動作とは直接関係なく、Goランタイムの内部的な動作によって発生します。したがって、この追加の割り当てを考慮せずに閾値を厳しく設定すると、GOMAXPROCS > 1
の環境でテストが不当に失敗することになります。
このコミットでは、閾値を5
から10
に引き上げることで、このランタイム起因の追加の6回の割り当てを許容するように変更しました。これにより、テストはGOMAXPROCS
の設定に関わらず安定して動作するようになり、真にテスト対象の関数が余分なメモリ割り当てを行っている場合にのみ失敗するようになります。
コアとなるコードの変更箇所
変更はsrc/pkg/reflect/all_test.go
ファイルの一箇所のみです。
--- a/src/pkg/reflect/all_test.go
+++ b/src/pkg/reflect/all_test.go
@@ -1569,9 +1569,10 @@ func noAlloc(t *testing.T, n int, f func(int)) {
}\n \t// A few allocs may happen in the testing package when GOMAXPROCS > 1, so don\'t\n \t// require zero mallocs.\n+\t// A new thread, one of which will be created if GOMAXPROCS>1, does 6 allocations.\n \truntime.ReadMemStats(memstats)\n \tmallocs := memstats.Mallocs - oldmallocs\n-\tif mallocs > 5 {\n+\tif mallocs > 10 {\n \t\tt.Fatalf(\"%d mallocs after %d iterations\", mallocs, n)\n \t}\n }\
- 変更前:
if mallocs > 5 {
- 変更後:
if mallocs > 10 {
- 追加されたコメント:
// A new thread, one of which will be created if GOMAXPROCS>1, does 6 allocations.
コアとなるコードの解説
変更されたコードは、noAlloc
関数内のメモリ割り当てチェックの条件式です。
// A few allocs may happen in the testing package when GOMAXPROCS > 1, so don't
// require zero mallocs.
// A new thread, one of which will be created if GOMAXPROCS>1, does 6 allocations.
runtime.ReadMemStats(memstats) // 現在のメモリ統計情報を取得
mallocs := memstats.Mallocs - oldmallocs // テスト対象関数実行中の割り当て数を計算
if mallocs > 10 { // 割り当て数が10を超えたらテスト失敗
t.Fatalf("%d mallocs after %d iterations", mallocs, n)
}
このコードスニペットは、noAlloc
関数がテスト対象の関数f
の実行によって発生したメモリ割り当ての総数(mallocs
)を計算し、その数が特定の閾値を超えていないかを検証する部分です。
runtime.ReadMemStats(memstats)
:memstats
はruntime.MemStats
型のポインタで、この関数呼び出しにより現在のGoランタイムのメモリ使用状況に関する詳細な統計情報がこの構造体に格納されます。mallocs := memstats.Mallocs - oldmallocs
:memstats.Mallocs
はプログラム開始からの総メモリ割り当て回数を示します。oldmallocs
はnoAlloc
関数が呼び出される前の総割り当て回数です。この差分により、noAlloc
関数内でテスト対象の関数が実行された期間に発生した純粋なメモリ割り当て回数が得られます。if mallocs > 10 { ... }
: ここが変更された条件式です。以前は5
でしたが、10
に変更されました。これは、GOMAXPROCS > 1
の環境でGoランタイムが新しいOSスレッドを起動する際に発生する約6回のメモリ割り当てを許容するためです。閾値を10に設定することで、この「ノイズ」を吸収し、テストがより堅牢になります。もしmallocs
が10を超えた場合、それはテスト対象の関数が予期せぬ余分なメモリ割り当てを行っていることを意味し、t.Fatalf
が呼び出されてテストは失敗します。
この変更により、reflect
パッケージのメモリ割り当てテストは、並行処理が有効な環境でも安定して動作するようになりました。
関連リンク
- Go Gerrit Change-ID:
https://golang.org/cl/6354063
- Goプロジェクトでは、GitHubのプルリクエストに相当するコードレビューシステムとしてGerritを使用しています。このリンクは、このコミットがGerrit上でレビューされた際の変更セット(Change-ID)を指します。
参考にした情報源リンク
- GitHub上のコミットページ: https://github.com/golang/go/commit/21de1ab359f198fff172f5e2bbaa29ab955a4688
- Go言語公式ドキュメント (reflectパッケージ): https://pkg.go.dev/reflect
- Go言語公式ドキュメント (runtimeパッケージ): https://pkg.go.dev/runtime
- Go言語公式ドキュメント (testingパッケージ): https://pkg.go.dev/testing
- Go言語におけるGOMAXPROCS: https://go.dev/doc/effective_go#concurrency (Effective GoのConcurrencyセクションなど)
- Go言語におけるメモリ管理とガベージコレクション: (一般的なGoのメモリ管理に関する情報源、例: Goの公式ブログや技術記事)
- Flaky Tests (不安定なテスト) について: (一般的なソフトウェアテストの概念に関する情報源)