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

[インデックス 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

変更の背景

このコミットは主に二つの問題に対処しています。

  1. メモリ割り当て(mallocs)カウントの不正確さ: Go言語のテストにおいて、特定の操作がどれくらいのメモリ割り当てを発生させるかを測定する際、GOMAXPROCSの値が1より大きい場合に、並行処理の影響で正確なカウントができないという問題がありました。複数のプロセッサ(またはOSスレッド)が同時に動作していると、メモリ割り当てのタイミングや順序が非決定論的になり、テスト結果が不安定になる可能性がありました。正確なパフォーマンス測定のためには、単一の実行環境で測定することが望ましいと判断されました。
  2. $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点です。

  1. 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スレッド上で実行されるようになり、並行処理による非決定論的な影響を排除し、より安定した正確な測定が可能になります。
  2. $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のパスを取得するようになります。これにより、テストの堅牢性と移植性が向上します。

  3. reflectパッケージのテストにおけるmallocsの許容値の変更: src/pkg/reflect/all_test.gonoAlloc関数において、mallocsの許容値が> 10から> 0に変更されました。

    -	if mallocs > 10 {
    +	if mallocs > 0 {
    

    これは、GOMAXPROCSを1に設定することで、テスト実行中の不要なメモリ割り当てが完全に排除されることを期待しているためと考えられます。以前はGOMAXPROCS > 1の場合にテストパッケージ内で少数の割り当てが発生する可能性があったため、ある程度の許容値が設けられていましたが、GOMAXPROCS=1に固定することで、より厳密なゼロ割り当てのチェックが可能になったことを示唆しています。

コアとなるコードの変更箇所

このコミットでは、主にGoの標準ライブラリのテストファイルに以下の変更が加えられています。

src/pkg/encoding/gob/timing_test.go

TestCountEncodeMallocsTestCountDecodeMallocs関数に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のテストコードで頻繁に見られるパターンです。

  1. runtime.GOMAXPROCS(1): この関数呼び出しは、現在のGOMAXPROCSの値を返すと同時に、GOMAXPROCSを1に設定します。これにより、Goスケジューラはテスト関数が実行されている間、単一のOSスレッドのみを使用するようになります。
  2. defer: deferキーワードは、その行の関数呼び出しを、囲んでいる関数(この場合はテスト関数)がリターンする直前に実行するようにスケジュールします。
  3. 組み合わせ: 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言語の公式ドキュメント
  • Go言語のソースコード
  • Go言語のIssueトラッカー (Issue #3690は直接見つかりませんでしたが、コミットメッセージから存在が示唆されています)
  • Go言語のGOMAXPROCSに関する一般的な情報源
  • Go言語のメモリ管理に関する一般的な情報源