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

[インデックス 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言語では、通常、makenewキーワード、あるいは複合リテラルなどによってヒープメモリが割り当てられます。Goランタイムはガベージコレクタによってメモリ管理を行いますが、テストの文脈では、特定の処理が不必要に多くのメモリ割り当てを行っていないか(パフォーマンスや効率の観点から)を検証することが重要になります。

テストの不安定性 (Flakiness)

「Flaky test」とは、同じコードに対して同じテストを複数回実行しても、成功したり失敗したりと結果が不安定なテストを指します。このようなテストは、並行処理のタイミング、環境変数、外部リソースの可用性、あるいはランダムな要素など、非決定的な要因によって引き起こされることが多いです。このコミットのケースでは、GOMAXPROCSの設定という環境要因がテストの不安定性を引き起こしていました。

技術的詳細

変更が行われたsrc/pkg/reflect/all_test.go内のnoAlloc関数は、特定の関数fが実行中にメモリ割り当てを行わないことを検証するためのヘルパー関数です。

この関数のロジックは以下の通りです。

  1. テスト対象の関数fを実行する前に、runtime.ReadMemStatsを呼び出して現在のメモリ統計情報(特にMallocs、つまりメモリ割り当ての総回数)を記録します。
  2. テスト対象の関数fn回実行します。
  3. 関数実行後に再度runtime.ReadMemStatsを呼び出し、メモリ割り当ての総回数を取得します。
  4. 前後のMallocsの差分を計算し、テスト対象の関数が実行中にどれだけのメモリ割り当てを行ったかを算出します。
  5. この差分(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): memstatsruntime.MemStats型のポインタで、この関数呼び出しにより現在のGoランタイムのメモリ使用状況に関する詳細な統計情報がこの構造体に格納されます。
  • mallocs := memstats.Mallocs - oldmallocs: memstats.Mallocsはプログラム開始からの総メモリ割り当て回数を示します。oldmallocsnoAlloc関数が呼び出される前の総割り当て回数です。この差分により、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)を指します。

参考にした情報源リンク