[インデックス 12076] ファイルの概要
このコミットは、Go言語の標準ライブラリ strconv
パッケージ内のテストコード itoa_test.go
において、メモリ割り当て(アロケーション)の計測テストの安定性を向上させるための変更です。具体的には、テスト開始前にガベージコレクション(GC)を明示的に実行することで、GCがテスト中のアロケーションカウントに与える不確定な影響を排除し、テストの信頼性を高めることを目的としています。
コミット
commit a53317668a9b29cf4633e67d1d83947eee92c951
Author: Russ Cox <rsc@golang.org>
Date: Sun Feb 19 22:13:04 2012 -0500
strconv: run garbage collection before counting allocations in test
My theory is that the call to f() allocates, which triggers
a garbage collection, which itself may do some allocation,
which is being counted. Running a garbage collection
before starting the test should avoid this problem.
Fixes #2894 (I hope).
R=golang-dev, bradfitz, nigeltao
CC=golang-dev
https://golang.org/cl/5685046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a53317668a9b29cf4633e67d1d83947eee92c951
元コミット内容
strconv
パッケージのテストにおいて、メモリ割り当てを計測する前にガベージコレクションを実行するように変更しました。これは、テスト対象の関数 f()
がメモリを割り当て、それがガベージコレクションをトリガーし、そのGC自体がさらにメモリを割り当てることで、テストのアロケーションカウントに影響を与えている可能性があるという仮説に基づいています。テスト開始前にGCを実行することで、この問題を回避し、テストの安定化を図ります。この変更は、Issue #2894 の修正を意図しています。
変更の背景
Go言語のテストスイートでは、特定の操作がどれくらいのメモリを割り当てるかを計測するテストが存在します。これは、パフォーマンス最適化やメモリ効率の維持において非常に重要です。しかし、このようなアロケーション計測テストは、ガベージコレクションの動作によって不安定になることがあります。
コミットメッセージにあるように、テスト対象の関数 f()
が実行されると、その中でメモリ割り当てが発生します。Goのランタイムは、メモリ使用量がある閾値を超えると自動的にガベージコレクションをトリガーします。このGCプロセス自体も、内部的に少量のメモリを割り当てることがあります。問題は、このGCによるアロケーションが、本来テストで計測したい「f()
関数によるアロケーション」に混入し、テスト結果を不正確にしたり、非決定的にしたりする可能性があったことです。
つまり、同じテストを複数回実行しても、GCがいつ、どのように発生するかに依存して、アロケーションの計測値が変動してしまう「flaky test」(不安定なテスト)になっていたと考えられます。このコミットは、この非決定性を排除し、アロケーション計測テストの信頼性を確保するために行われました。
前提知識の解説
Go言語のメモリ管理とガベージコレクション (GC)
Go言語は、自動メモリ管理(ガベージコレクション)を採用しています。開発者はC++のように手動でメモリを解放する必要がなく、Goランタイムが不要になったメモリ領域を自動的に回収します。
- ヒープとスタック: Goプログラムが使用するメモリは、主に「スタック」と「ヒープ」に分けられます。
- スタック: 関数呼び出しやローカル変数など、生存期間が短いデータが格納されます。コンパイル時にサイズが決定されることが多く、高速にアクセスできます。
- ヒープ: プログラムの実行中に動的に確保されるメモリ領域です。
make
やnew
などで確保されたデータや、関数から返されるポインタなどがヒープに割り当てられます。ヒープに割り当てられたデータは、GCによって管理されます。
- ガベージコレクション (GC): GoのGCは、並行マーク&スイープ方式を採用しています。これは、プログラムの実行と並行してGCが動作し、アプリケーションの停止時間(ストップ・ザ・ワールド)を最小限に抑えることを目指しています。GCは、到達可能なオブジェクト(参照されているオブジェクト)をマークし、マークされなかったオブジェクト(不要になったオブジェクト)をスイープ(回収)します。
- GCのトリガー: GCは、主に以下の条件で自動的にトリガーされます。
- ヒープの使用量が、前回のGC後のヒープ使用量に対して一定の割合(デフォルトでは2倍)を超えた場合。
runtime.GC()
関数が明示的に呼び出された場合。
runtime.MemStats
と runtime.ReadMemStats
Go言語の runtime
パッケージは、ランタイムに関する低レベルな情報や操作を提供します。
runtime.MemStats
は、Goランタイムのメモリ統計情報を含む構造体です。これには、ヒープの使用量、アロケーションの総数、GCの実行回数など、様々なメモリ関連のメトリクスが含まれています。
runtime.MemStats
構造体には、以下のようなフィールドがあります(一部抜粋):Alloc
: 現在ヒープに割り当てられているバイト数。TotalAlloc
: プログラム開始からの総割り当てバイト数。Sys
: ランタイムがOSから取得した総バイト数。Mallocs
: 割り当てられたオブジェクトの総数。Frees
: 解放されたオブジェクトの総数。NumGC
: 実行されたGCの総回数。
runtime.ReadMemStats(m *MemStats)
関数は、現在のメモリ統計情報を引数で渡された MemStats
構造体に書き込みます。この関数を呼び出すことで、プログラムは実行時のメモリ使用状況を詳細に把握することができます。
runtime.GC()
runtime.GC()
関数は、Goランタイムに対してガベージコレクションを明示的に実行するように要求します。通常、GCはランタイムによって自動的に管理されるため、この関数を明示的に呼び出すことは稀です。しかし、特定の状況(例えば、メモリ使用量を一時的に最小化したい場合や、今回のケースのようにテストの安定性を確保したい場合)では有用です。
runtime.GC()
を呼び出すと、ランタイムは可能な限り早くGCを実行しようとします。これにより、ヒープ上の不要なオブジェクトが回収され、メモリが解放されます。
テストにおけるアロケーション計測の難しさ
Goのテストフレームワークでは、ベンチマークテストなどでアロケーション数を計測する機能がありますが、通常の単体テストで厳密なアロケーション数を計測するのは難しい場合があります。その主な理由は、GCの非決定的な動作です。
- GCのタイミング: GCは、ヒープの使用状況やランタイムの内部的な判断に基づいて非同期に実行されます。そのため、テストが実行されるたびにGCのタイミングが異なり、テスト対象のコードが実行される前、途中、または後にGCが発生する可能性があります。
- GC自身のアロケーション: GCプロセス自体も、内部的なデータ構造の管理のために一時的にメモリを割り当てることがあります。このGCによるアロケーションが、テスト対象のコードによるアロケーションと混同されると、正確な計測ができなくなります。
このコミットは、まさにこの「GC自身のアロケーションがテスト結果に影響を与える」という問題を解決しようとしています。
技術的詳細
このコミットで変更された numAllocations
関数は、Goのテストにおいて、特定の関数 f()
が実行された際に発生するメモリ割り当ての数を計測するために使用されるユーティリティ関数です。
変更前の numAllocations
関数は、以下の手順でアロケーション数を計測していました。
runtime.ReadMemStats
を呼び出して、関数f()
実行前のメモリ統計情報(特にMallocs
、つまり割り当てられたオブジェクトの総数)を取得する。- 引数として渡された関数
f()
を実行する。 - 再度
runtime.ReadMemStats
を呼び出して、関数f()
実行後のメモリ統計情報を取得する。 f()
実行前後のMallocs
の差分を計算し、それをf()
によるアロケーション数として返す。
この計測方法の問題点は、f()
の実行中にGCがトリガーされ、そのGC自体がメモリを割り当てた場合、そのGCによるアロケーションも f()
のアロケーションとしてカウントされてしまうことでした。コミットメッセージにある「My theory is that the call to f() allocates, which triggers a garbage collection, which itself may do some allocation, which is being counted.」という記述は、この問題を明確に指摘しています。
この問題を解決するために、コミットでは numAllocations
関数の冒頭に runtime.GC()
の呼び出しが追加されました。
func numAllocations(f func()) int {
runtime.GC() // 追加された行
memstats := new(runtime.MemStats)
runtime.ReadMemStats(memstats)
n0 := memstats.Mallocs
f()
runtime.ReadMemStats(memstats)
n1 := memstats.Mallocs
return int(n1 - n0)
}
この変更により、numAllocations
関数が呼び出されると、まず最初に明示的にガベージコレクションが実行されます。これにより、テスト対象の関数 f()
が実行される前に、ランタイムが保持している不要なオブジェクトがすべて回収され、ヒープがクリーンな状態になります。また、GC自体が内部的に行う可能性のあるアロケーションも、f()
の実行前に完了するため、その後の f()
による純粋なアロケーション数をより正確に計測できるようになります。
つまり、runtime.GC()
を事前に呼び出すことで、GCによるノイズを排除し、アロケーション計測テストの再現性と信頼性を向上させているのです。
コアとなるコードの変更箇所
--- a/src/pkg/strconv/itoa_test.go
+++ b/src/pkg/strconv/itoa_test.go
@@ -127,6 +127,7 @@ func TestUitoa(t *testing.T) {
}
func numAllocations(f func()) int {
+\truntime.GC()
memstats := new(runtime.MemStats)
runtime.ReadMemStats(memstats)
n0 := memstats.Mallocs
コアとなるコードの解説
変更は src/pkg/strconv/itoa_test.go
ファイル内の numAllocations
関数に1行追加されただけです。
追加された行: runtime.GC()
この1行が追加されたことで、numAllocations
関数が実行されるたびに、まずGoランタイムのガベージコレクタが強制的に実行されます。これにより、numAllocations
関数が計測を開始する前に、システムが可能な限りクリーンなメモリ状態になり、GCが原因で発生する可能性のある余分なメモリ割り当てが事前に処理されます。
結果として、その後に続く f()
関数の実行によって発生するメモリ割り当てのみが、memstats.Mallocs
の差分として正確に計測されるようになります。これは、テストの再現性を高め、GCのタイミングに依存しない安定したアロケーション計測を可能にするための重要な修正です。
関連リンク
このコミットが修正を意図しているIssue #2894は、Goの古いIssueトラッカーシステムに存在していた可能性があり、現在のGitHubのIssueトラッカーでは直接見つけることができませんでした。しかし、GoプロジェクトのIssueトラッカーは通常、https://github.com/golang/go/issues で管理されています。
参考にした情報源リンク
- Go言語の公式ドキュメント:
runtime
パッケージ: https://pkg.go.dev/runtimeruntime.MemStats
構造体: https://pkg.go.dev/runtime#MemStatsruntime.ReadMemStats
関数: https://pkg.go.dev/runtime#ReadMemStatsruntime.GC
関数: https://pkg.go.dev/runtime#GC
- Go言語のガベージコレクションに関する一般的な情報源(例: Goのメモリ管理やGCの仕組みを解説しているブログ記事や書籍など)