[インデックス 14533] ファイルの概要
このコミットは、Go言語のランタイムにおけるメモリ割り当て(mallocs)のカウント方法の改善と、$GOROOT
環境変数に依存していたテストの修正を目的としています。特に、並行処理がメモリ割り当ての正確なカウントに与える影響を考慮し、テスト実行時のGOMAXPROCS
の値を一時的に1に設定することで、より信頼性の高い測定を可能にしています。
コミット
commit 9e30b708a1123a6c1c7ee52992b976795c786235
Author: Shenghou Ma <minux.ma@gmail.com>
Date: Sat Dec 1 00:38:01 2012 +0800
all: set GOMAXPROCS to 1 when counting mallocs
also fix an annoying test that relies on $GOROOT be set.
Fixes #3690.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/6844086
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9e30b708a1123a6c1c7ee52992b976795c786235
元コミット内容
all: set GOMAXPROCS to 1 when counting mallocs
also fix an annoying test that relies on $GOROOT be set.
Fixes #3690.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/6844086
変更の背景
このコミットは主に二つの問題に対処しています。
- メモリ割り当て(mallocs)カウントの不正確さ: Go言語のテストにおいて、特定の操作がどれくらいのメモリ割り当てを発生させるかを測定する際、
GOMAXPROCS
の値が1より大きい場合に、並行処理の影響で正確なカウントができないという問題がありました。複数のプロセッサ(またはOSスレッド)が同時に動作していると、メモリ割り当てのタイミングや順序が非決定論的になり、テスト結果が不安定になる可能性がありました。正確なパフォーマンス測定のためには、単一の実行環境で測定することが望ましいと判断されました。 $GOROOT
環境変数への依存: 一部のテストが、Goのインストールパスを示す$GOROOT
環境変数に直接依存していました。これは、テストが実行される環境によって$GOROOT
が設定されていない場合や、異なる値が設定されている場合にテストが失敗する原因となっていました。Goのテストは、環境変数に依存せず、Goランタイムが提供するAPIを通じて必要な情報を取得するべきです。
これらの問題は、Goのテストの信頼性と移植性を低下させるものであり、修正が必要とされました。特に、Fixes #3690
という記述から、GoのIssueトラッカーに登録された問題(Issue 3690)を解決するためのコミットであることが示唆されています。
前提知識の解説
Go言語の並行処理とGOMAXPROCS
Go言語は、軽量なスレッドである「Goroutine」と、それらをOSスレッドにマッピングする「Goスケジューラ」によって並行処理を実現しています。GOMAXPROCS
は、Goランタイムが同時に実行できるOSスレッドの最大数を制御する環境変数または関数です。
GOMAXPROCS
のデフォルト値は、Go 1.5以降では利用可能なCPUコア数に設定されます。それ以前のバージョンでは1でした。GOMAXPROCS
が1より大きい場合、複数のGoroutineが並行して実行される可能性があり、これによりメモリ割り当てのタイミングが非決定論的になることがあります。
メモリ割り当て(mallocs)のカウント
Go言語では、プログラムが実行中に動的にメモリを確保する際に「メモリ割り当て(malloc)」が発生します。パフォーマンスチューニングやリソース使用量の最適化において、特定の処理がどれくらいのメモリ割り当てを発生させるかを正確に把握することは非常に重要です。Goのruntime
パッケージには、runtime.ReadMemStats
などの関数があり、これらを使ってメモリ統計情報を取得できます。
GOROOT
環境変数とruntime.GOROOT()
GOROOT
は、GoのSDKがインストールされているディレクトリのパスを示す環境変数です。Goのツールチェーンやライブラリは、このパスを基に動作します。しかし、テストコードが直接os.Getenv("GOROOT")
のように環境変数を読み取ることは、テストの独立性を損なう可能性があります。
Goのruntime
パッケージには、runtime.GOROOT()
という関数が提供されており、これはGoランタイムが認識しているGOROOT
のパスを返します。この関数を使用することで、環境変数に直接依存することなく、Goのインストールパスを取得できます。
技術的詳細
このコミットの主要な技術的変更は以下の2点です。
-
GOMAXPROCS
の一時的な設定: メモリ割り当てをカウントするテスト関数(例:TestCountEncodeMallocs
,TestCountMallocs
,noAlloc
など)の冒頭に、以下のコードが追加されています。defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
この行は、テスト関数が実行される際に
GOMAXPROCS
を一時的に1に設定し、関数が終了する際に元の値に戻すという慣用的なパターンです。runtime.GOMAXPROCS(1)
: 現在のGOMAXPROCS
の値を返し、GOMAXPROCS
を1に設定します。defer
:defer
キーワードにより、runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
の呼び出しは、その関数がリターンする直前に実行されます。これにより、GOMAXPROCS
はテスト関数が開始される前に1に設定され、テスト関数が終了した後に元の値に戻されます。 これにより、メモリ割り当てのカウントが単一のOSスレッド上で実行されるようになり、並行処理による非決定論的な影響を排除し、より安定した正確な測定が可能になります。
-
$GOROOT
環境変数への依存の排除:src/pkg/path/filepath/path_test.go
内のTestBug3486
関数において、os.Getenv("GOROOT")
を使用していた箇所がruntime.GOROOT()
に置き換えられました。- root, err := filepath.EvalSymlinks(os.Getenv("GOROOT")) + root, err := filepath.EvalSymlinks(runtime.GOROOT())
この変更により、テストは環境変数
$GOROOT
の有無や値に依存しなくなり、Goランタイムが提供する公式なAPIを通じてGOROOT
のパスを取得するようになります。これにより、テストの堅牢性と移植性が向上します。 -
reflect
パッケージのテストにおけるmallocsの許容値の変更:src/pkg/reflect/all_test.go
のnoAlloc
関数において、mallocsの許容値が> 10
から> 0
に変更されました。- if mallocs > 10 { + if mallocs > 0 {
これは、
GOMAXPROCS
を1に設定することで、テスト実行中の不要なメモリ割り当てが完全に排除されることを期待しているためと考えられます。以前はGOMAXPROCS > 1
の場合にテストパッケージ内で少数の割り当てが発生する可能性があったため、ある程度の許容値が設けられていましたが、GOMAXPROCS=1
に固定することで、より厳密なゼロ割り当てのチェックが可能になったことを示唆しています。
コアとなるコードの変更箇所
このコミットでは、主にGoの標準ライブラリのテストファイルに以下の変更が加えられています。
src/pkg/encoding/gob/timing_test.go
TestCountEncodeMallocs
とTestCountDecodeMallocs
関数にdefer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
が追加されました。
--- a/src/pkg/encoding/gob/timing_test.go
+++ b/src/pkg/encoding/gob/timing_test.go
@@ -50,6 +50,7 @@ func BenchmarkEndToEndByteBuffer(b *testing.B) {
}
func TestCountEncodeMallocs(t *testing.T) {
+ defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
var buf bytes.Buffer
enc := NewEncoder(&buf)
bench := &Bench{7, 3.2, "now is the time", []byte("for all good men")}
@@ -69,6 +70,7 @@ func TestCountEncodeMallocs(t *testing.T) {
}
func TestCountDecodeMallocs(t *testing.T) {
+ defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
var buf bytes.Buffer
enc := NewEncoder(&buf)
bench := &Bench{7, 3.2, "now is the time", []byte("for all good men")}
src/pkg/fmt/fmt_test.go
TestCountMallocs
関数にdefer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
が追加されました。
--- a/src/pkg/fmt/fmt_test.go
+++ b/src/pkg/fmt/fmt_test.go
@@ -581,6 +581,7 @@ var mallocTest = []struct {
var _ bytes.Buffer
func TestCountMallocs(t *testing.T) {
+ defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
for _, mt := range mallocTest {
const N = 100
memstats := new(runtime.MemStats)
src/pkg/math/big/nat_test.go
TestMulUnbalanced
関数にdefer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
が追加されました。
--- a/src/pkg/math/big/nat_test.go
+++ b/src/pkg/math/big/nat_test.go
@@ -180,6 +180,7 @@ func allocBytes(f func()) uint64 {
// does not cause deep recursion and in turn allocate too much memory.
// Test case for issue 3807.
func TestMulUnbalanced(t *testing.T) {
+ defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
x := rndNat(50000)
y := rndNat(40)
allocSize := allocBytes(func() {
src/pkg/net/http/header_test.go
doHeaderWriteSubset
関数にdefer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
が追加されました。また、コメントが修正されています。
--- a/src/pkg/net/http/header_test.go
+++ b/src/pkg/net/http/header_test.go
@@ -188,6 +188,7 @@ type errorfer interface {
}
func doHeaderWriteSubset(n int, t errorfer) {
+ defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
h := Header(map[string][]string{
"Content-Length": {"123"},
"Content-Type": {"text/plain"},
@@ -204,7 +205,7 @@ func doHeaderWriteSubset(n int, t errorfer) {
var m1 runtime.MemStats
runtime.ReadMemStats(&m1)
if mallocs := m1.Mallocs - m0.Mallocs; n >= 100 && mallocs >= uint64(n) {
- // TODO(bradfitz,rsc): once we can sort with allocating,
+ // TODO(bradfitz,rsc): once we can sort without allocating,
// make this an error. See http://golang.org/issue/3761
// t.Errorf("did %d mallocs (>= %d iterations); should have avoided mallocs", mallocs, n)
}
src/pkg/net/rpc/server_test.go
countMallocs
関数にdefer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
が追加されました。
--- a/src/pkg/net/rpc/server_test.go
+++ b/src/pkg/net/rpc/server_test.go
@@ -446,6 +446,7 @@ func dialHTTP() (*Client, error) {
}
func countMallocs(dial func() (*Client, error), t *testing.T) uint64 {
+ defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
once.Do(startServer)
client, err := dial()
if err != nil {
src/pkg/path/filepath/path_test.go
TestClean
関数にdefer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
が追加されました。また、TestBug3486
関数でos.Getenv("GOROOT")
がruntime.GOROOT()
に置き換えられました。
--- a/src/pkg/path/filepath/path_test.go
+++ b/src/pkg/path/filepath/path_test.go
@@ -91,6 +91,7 @@ var wincleantests = []PathTest{
}
func TestClean(t *testing.T) {
+ defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
tests := cleantests
if runtime.GOOS == "windows" {
for i := range tests {
@@ -897,7 +898,7 @@ func TestDriveLetterInEvalSymlinks(t *testing.T) {
}
func TestBug3486(t *testing.T) { // http://code.google.com/p/go/issues/detail?id=3486
- root, err := filepath.EvalSymlinks(os.Getenv("GOROOT"))
+ root, err := filepath.EvalSymlinks(runtime.GOROOT())
if err != nil {
t.Fatal(err)
}
src/pkg/path/path_test.go
TestClean
関数にdefer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
が追加されました。
--- a/src/pkg/path/path_test.go
+++ b/src/pkg/path/path_test.go
@@ -64,6 +64,7 @@ var cleantests = []PathTest{
}
func TestClean(t *testing.T) {
+ defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
for _, test := range cleantests {
if s := Clean(test.path); s != test.result {
t.Errorf("Clean(%q) = %q, want %q", test.path, s, test.result)
src/pkg/reflect/all_test.go
noAlloc
関数にdefer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
が追加され、mallocsの許容値が変更されました。
--- a/src/pkg/reflect/all_test.go
+++ b/src/pkg/reflect/all_test.go
@@ -2012,6 +2012,7 @@ func TestAddr(t *testing.T) {
}
func noAlloc(t *testing.T, n int, f func(int)) {
+ defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
// once to prime everything
f(-1)
memstats := new(runtime.MemStats)
@@ -2021,12 +2022,9 @@ func noAlloc(t *testing.T, n int, f func(int)) {
for j := 0; j < n; j++ {
f(j)
}
- // 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 {
+ if mallocs > 0 {
t.Fatalf("%d mallocs after %d iterations", mallocs, n)
}
}
src/pkg/runtime/gc_test.go
TestGcSys
関数にdefer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
が追加されました。
--- a/src/pkg/runtime/gc_test.go
+++ b/src/pkg/runtime/gc_test.go
@@ -10,6 +10,7 @@ import (
)
func TestGcSys(t *testing.T) {
+ defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
memstats := new(runtime.MemStats)
runtime.GC()
runtime.ReadMemStats(memstats)
src/pkg/runtime/mallocrep1.go
AllocAndFree
関数にdefer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
が追加されました。
--- a/src/pkg/runtime/mallocrep1.go
+++ b/src/pkg/runtime/mallocrep1.go
@@ -39,6 +39,7 @@ func OkAmount(size, n uintptr) bool {
}
func AllocAndFree(size, count int) {
+ defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
if *chatty {
fmt.Printf("size=%d count=%d ...\n", size, count)
}
src/pkg/strconv/strconv_test.go
TestCountMallocs
関数にdefer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
が追加されました。
--- a/src/pkg/strconv/strconv_test.go
+++ b/src/pkg/strconv/strconv_test.go
@@ -44,6 +44,7 @@ var (
)
func TestCountMallocs(t *testing.T) {
+ defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
for _, mt := range mallocTest {
const N = 100
memstats := new(runtime.MemStats)
コアとなるコードの解説
このコミットの核となる変更は、Goのテストにおけるメモリ割り当ての測定精度を向上させるためのGOMAXPROCS
の制御と、テストの環境依存性を排除するためのGOROOT
の取得方法の変更です。
defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
このイディオムは、Goのテストコードで頻繁に見られるパターンです。
runtime.GOMAXPROCS(1)
: この関数呼び出しは、現在のGOMAXPROCS
の値を返すと同時に、GOMAXPROCS
を1に設定します。これにより、Goスケジューラはテスト関数が実行されている間、単一のOSスレッドのみを使用するようになります。defer
:defer
キーワードは、その行の関数呼び出しを、囲んでいる関数(この場合はテスト関数)がリターンする直前に実行するようにスケジュールします。- 組み合わせ:
defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
とすることで、テスト関数が開始される前にGOMAXPROCS
が1に設定され、テスト関数が終了する際には、最初にruntime.GOMAXPROCS(1)
が返した元のGOMAXPROCS
の値に自動的に戻されます。
このメカニズムにより、メモリ割り当てのカウントが、並行処理による影響を受けない単一スレッド環境で実行されることが保証されます。これにより、テスト結果の再現性と信頼性が大幅に向上します。特に、メモリ割り当ての回数や量が厳密にチェックされるようなパフォーマンス関連のテストにおいて、この設定は不可欠です。
os.Getenv("GOROOT")
から runtime.GOROOT()
への変更
src/pkg/path/filepath/path_test.go
におけるこの変更は、テストの堅牢性を高めるためのものです。
os.Getenv("GOROOT")
: これは、オペレーティングシステムの環境変数GOROOT
の値を直接読み取ります。もしこの環境変数が設定されていない場合や、誤った値が設定されている場合、テストは期待通りに動作しません。これは、テストが実行される環境に依存してしまうため、CI/CD環境や異なる開発者のマシンでのテスト実行時に問題を引き起こす可能性があります。runtime.GOROOT()
: これはGoランタイムが提供する関数であり、Goのインストールパスをプログラム的に取得します。この関数は、Goランタイム自身が認識しているGOROOT
のパスを返すため、環境変数の設定に依存せず、常に正しいパスを提供します。
この変更により、テストはより自己完結的になり、外部環境への依存が減少します。これは、Goのテストフレームワークが推奨するプラクティスであり、テストの信頼性と移植性を向上させます。
関連リンク
- Go言語の
runtime
パッケージ: https://pkg.go.dev/runtime - Go言語の
os
パッケージ: https://pkg.go.dev/os - Go言語の
testing
パッケージ: https://pkg.go.dev/testing
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード
- Go言語のIssueトラッカー (Issue #3690は直接見つかりませんでしたが、コミットメッセージから存在が示唆されています)
- Go言語の
GOMAXPROCS
に関する一般的な情報源 - Go言語のメモリ管理に関する一般的な情報源