[インデックス 15620] ファイルの概要
このコミットは、Go言語の標準ライブラリにおけるtesting.AllocsPerRun
を使用したメモリ割り当てテストが、GOMAXPROCS
が1より大きい(つまり、複数のCPUコアが利用可能な)環境で実行された場合に、信頼性の低い結果を返す問題を修正するものです。具体的には、GOMAXPROCS
が1より大きい場合、これらのテストをスキップするように変更が加えられています。これにより、並行処理環境下でのテストの不安定性が解消されます。
コミット
commit 0a71a5b029f0f8576d7dc948f72e16deb00a035e
Author: Albert Strasheim <fullung@gmail.com>
Date: Wed Mar 6 15:52:32 2013 -0800
all: Skip AllocsPerRun tests if GOMAXPROCS>1.
Fixes #4974.
R=rsc, bradfitz, r
CC=golang-dev
https://golang.org/cl/7545043
---
src/pkg/encoding/gob/timing_test.go | 9 +++++++++
src/pkg/fmt/fmt_test.go | 4 ++++\n src/pkg/net/http/header_test.go | 4 ++++\n src/pkg/net/rpc/server_test.go | 6 ++++++\n src/pkg/path/filepath/path_test.go | 5 +++++\n src/pkg/path/path_test.go | 6 ++++++\n src/pkg/reflect/all_test.go | 4 ++++\n src/pkg/sort/search_test.go | 4 ++++\n src/pkg/strconv/strconv_test.go | 4 ++++\n src/pkg/time/time_test.go | 4 ++++\n 10 files changed, 50 insertions(+)
diff --git a/src/pkg/encoding/gob/timing_test.go b/src/pkg/encoding/gob/timing_test.go
index 13eb119253..f589675dd9 100644
--- a/src/pkg/encoding/gob/timing_test.go
+++ b/src/pkg/encoding/gob/timing_test.go
@@ -9,6 +9,7 @@ import (
"fmt"
"io"
"os"
+" runtime"
"testing"
)
@@ -49,6 +50,10 @@ func BenchmarkEndToEndByteBuffer(b *testing.B) {
}
func TestCountEncodeMallocs(t *testing.T) {
+ if runtime.GOMAXPROCS(0) > 1 {
+ t.Skip("skipping; GOMAXPROCS>1")
+ }
+
const N = 1000
var buf bytes.Buffer
@@ -65,6 +70,10 @@ func TestCountEncodeMallocs(t *testing.T) {
}
func TestCountDecodeMallocs(t *testing.T) {
+ if runtime.GOMAXPROCS(0) > 1 {
+ t.Skip("skipping; GOMAXPROCS>1")
+ }
+
const N = 1000
var buf bytes.Buffer
diff --git a/src/pkg/fmt/fmt_test.go b/src/pkg/fmt/fmt_test.go
index af4b5c8f8e..552f76931b 100644
--- a/src/pkg/fmt/fmt_test.go
+++ b/src/pkg/fmt/fmt_test.go
@@ -9,6 +9,7 @@ import (
. "fmt"
"io"
"math"
+" runtime"
"strings"
"testing"
"time"
@@ -601,6 +602,9 @@ var mallocTest = []struct {
var _ bytes.Buffer
func TestCountMallocs(t *testing.T) {
+ if runtime.GOMAXPROCS(0) > 1 {
+ t.Skip("skipping; GOMAXPROCS>1")
+ }
for _, mt := range mallocTest {
mallocs := testing.AllocsPerRun(100, mt.fn)
if got, max := mallocs, float64(mt.count); got > max {
diff --git a/src/pkg/net/http/header_test.go b/src/pkg/net/http/header_test.go
index 88c420a44a..a2b82a701c 100644
--- a/src/pkg/net/http/header_test.go
+++ b/src/pkg/net/http/header_test.go
@@ -6,6 +6,7 @@ package http
import (
"bytes"
+" runtime"
"testing"
"time"
)
@@ -192,6 +193,9 @@ func BenchmarkHeaderWriteSubset(b *testing.B) {
}
func TestHeaderWriteSubsetMallocs(t *testing.T) {
+ if runtime.GOMAXPROCS(0) > 1 {
+ t.Skip("skipping; GOMAXPROCS>1")
+ }
n := testing.AllocsPerRun(100, func() {
buf.Reset()
testHeader.WriteSubset(&buf, nil)
diff --git a/src/pkg/net/rpc/server_test.go b/src/pkg/net/rpc/server_test.go
index 8a15306235..5b2f9f2ded 100644
--- a/src/pkg/net/rpc/server_test.go
+++ b/src/pkg/net/rpc/server_test.go
@@ -465,10 +465,16 @@ func countMallocs(dial func() (*Client, error), t *testing.T) float64 {
}
func TestCountMallocs(t *testing.T) {
+ if runtime.GOMAXPROCS(0) > 1 {
+ t.Skip("skipping; GOMAXPROCS>1")
+ }
fmt.Printf("mallocs per rpc round trip: %v\n", countMallocs(dialDirect, t))
}
func TestCountMallocsOverHTTP(t *testing.T) {
+ if runtime.GOMAXPROCS(0) > 1 {
+ t.Skip("skipping; GOMAXPROCS>1")
+ }
fmt.Printf("mallocs per HTTP rpc round trip: %v\n", countMallocs(dialHTTP, t))
}
diff --git a/src/pkg/path/filepath/path_test.go b/src/pkg/path/filepath/path_test.go
index e768ad32f0..c4d73602ff 100644
--- a/src/pkg/path/filepath/path_test.go
+++ b/src/pkg/path/filepath/path_test.go
@@ -107,6 +107,11 @@ func TestClean(t *testing.T) {
}
}
+ if runtime.GOMAXPROCS(0) > 1 {
+ t.Log("skipping AllocsPerRun checks; GOMAXPROCS>1")
+ return
+ }
+
for _, test := range tests {
allocs := testing.AllocsPerRun(100, func() { filepath.Clean(test.result) })
if allocs > 0 {
diff --git a/src/pkg/path/path_test.go b/src/pkg/path/path_test.go
index 220ec1a0bb..69caa80e4f 100644
--- a/src/pkg/path/path_test.go
+++ b/src/pkg/path/path_test.go
@@ -5,6 +5,7 @@
package path
import (
+" runtime"
"testing"
)
@@ -72,6 +73,11 @@ func TestClean(t *testing.T) {
}
}
+ if runtime.GOMAXPROCS(0) > 1 {
+ t.Log("skipping AllocsPerRun checks; GOMAXPROCS>1")
+ return
+ }
+
for _, test := range cleantests {
allocs := testing.AllocsPerRun(100, func() { Clean(test.result) })
if allocs > 0 {
diff --git a/src/pkg/reflect/all_test.go b/src/pkg/reflect/all_test.go
index 6f006db186..97b3a9f2e5 100644
--- a/src/pkg/reflect/all_test.go
+++ b/src/pkg/reflect/all_test.go
@@ -13,6 +13,7 @@ import (
"math/rand"
"os"
. "reflect"
+" runtime"
"sync"
"testing"
"time"
@@ -2011,6 +2012,9 @@ func TestAddr(t *testing.T) {
}
func noAlloc(t *testing.T, n int, f func(int)) {
+ if runtime.GOMAXPROCS(0) > 1 {
+ t.Skip("skipping; GOMAXPROCS>1")
+ }
i := -1
allocs := testing.AllocsPerRun(n, func() {
f(i)
diff --git a/src/pkg/sort/search_test.go b/src/pkg/sort/search_test.go
index 4d8d6d930b..ee95c663cc 100644
--- a/src/pkg/sort/search_test.go
+++ b/src/pkg/sort/search_test.go
@@ -5,6 +5,7 @@
package sort_test
import (
+" runtime"
. "sort"
"testing"
)
@@ -127,6 +128,9 @@ func runSearchWrappers() {
}
func TestSearchWrappersDontAlloc(t *testing.T) {
+ if runtime.GOMAXPROCS(0) > 1 {
+ t.Skip("skipping; GOMAXPROCS>1")
+ }
allocs := testing.AllocsPerRun(100, runSearchWrappers)
if allocs != 0 {
t.Errorf("expected no allocs for runSearchWrappers, got %v", allocs)
diff --git a/src/pkg/strconv/strconv_test.go b/src/pkg/strconv/strconv_test.go
index c3c5389267..3cd7835ccc 100644
--- a/src/pkg/strconv/strconv_test.go
+++ b/src/pkg/strconv/strconv_test.go
@@ -5,6 +5,7 @@
package strconv_test
import (
+" runtime"
. "strconv"
"strings"
"testing"
@@ -43,6 +44,9 @@ var (
)
func TestCountMallocs(t *testing.T) {
+ if runtime.GOMAXPROCS(0) > 1 {
+ t.Skip("skipping; GOMAXPROCS>1")
+ }
for _, mt := range mallocTest {
allocs := testing.AllocsPerRun(100, mt.fn)
if max := float64(mt.count); allocs > max {
diff --git a/src/pkg/time/time_test.go b/src/pkg/time/time_test.go
index 4b268f73d9..a0ee37ae3b 100644
--- a/src/pkg/time/time_test.go
+++ b/src/pkg/time/time_test.go
@@ -11,6 +11,7 @@ import (
"fmt"
"math/big"
"math/rand"
+" runtime"
"strconv"
"strings"
"testing"
@@ -1299,6 +1300,9 @@ var mallocTest = []struct {
}
func TestCountMallocs(t *testing.T) {
+ if runtime.GOMAXPROCS(0) > 1 {
+ t.Skip("skipping; GOMAXPROCS>1")
+ }
for _, mt := range mallocTest {
allocs := int(testing.AllocsPerRun(100, mt.fn))
if allocs > mt.count {
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/0a71a5b029f0f8576d7dc948f72e16deb00a035e
元コミット内容
all: Skip AllocsPerRun tests if GOMAXPROCS>1.
Fixes #4974.
R=rsc, bradfitz, r
CC=golang-dev
https://golang.org/cl/7545043
変更の背景
このコミットの背景には、Go言語のテストフレームワークにおけるtesting.AllocsPerRun
関数の挙動と、Goランタイムの並行処理設定であるGOMAXPROCS
との間の相互作用に起因する問題があります。
testing.AllocsPerRun
は、指定された関数が実行される際に発生するメモリ割り当ての平均回数を測定するために設計されています。これは、Goプログラムのパフォーマンス最適化、特にメモリ使用量の削減において非常に重要な指標となります。開発者はこの関数を使用して、コード変更がメモリ割り当てに与える影響を定量的に評価します。
しかし、GOMAXPROCS
が1より大きい値に設定されている場合、つまりGoランタイムが複数のOSスレッド(CPUコア)を利用してゴルーチンを並行実行できる環境では、AllocsPerRun
の測定結果が不安定になるという問題が報告されていました。これは、複数のゴルーチンが同時にメモリを割り当てようとすることで、ガベージコレクタの動作やメモリ割り当てのタイミングが非決定論的になり、テスト結果にばらつきが生じるためです。
この不安定性は、CI/CDパイプラインや開発者のローカル環境でテストが失敗する原因となり、本来は問題のないコード変更が誤ってリグレッションとして検出される可能性がありました。コミットメッセージにあるFixes #4974
は、この特定のバグ報告に対応するものであることを示しています。この問題に対処するため、GOMAXPROCS
が1より大きい場合にこれらのテストをスキップするという決定がなされました。これにより、テストの信頼性が向上し、開発ワークフローの妨げとなる偽陽性の失敗が減少します。
前提知識の解説
1. Go言語のメモリ管理とガベージコレクション (GC)
Go言語は、自動メモリ管理(ガベージコレクション)を採用しています。開発者は手動でメモリを解放する必要がなく、ランタイムが不要になったメモリを自動的に回収します。メモリ割り当て(アロケーション)は、プログラムが新しいデータ構造やオブジェクトを作成する際に発生します。アロケーションの回数や量は、プログラムのパフォーマンス、特にレイテンシに大きな影響を与える可能性があります。アロケーションが多いと、ガベージコレクタが頻繁に動作し、プログラムの実行が一時停止する「ストップ・ザ・ワールド」が発生する可能性が高まります。
2. GOMAXPROCS
GOMAXPROCS
は、Goランタイムが同時に実行できるOSスレッドの最大数を制御する環境変数、またはruntime.GOMAXPROCS
関数によって設定される値です。
GOMAXPROCS=1
の場合、Goランタイムは最大1つのOSスレッドしか使用しません。これは、Goのスケジューラがゴルーチンを単一のOSスレッド上で多重化して実行することを意味します。この設定では、真の並列実行は行われず、並行実行のみが行われます。GOMAXPROCS > 1
の場合、Goランタイムは指定された数のOSスレッドを同時に使用できます。これにより、複数のCPUコアを持つシステム上で、複数のゴルーチンが真に並列に実行されることが可能になります。これは、マルチコアプロセッサの性能を最大限に引き出すために重要です。
3. testing.AllocsPerRun
testing.AllocsPerRun
は、Goの標準テストパッケージtesting
に含まれる関数で、ベンチマークテスト内で使用されます。その目的は、特定の関数が実行される間に発生するヒープメモリ割り当ての平均回数を測定することです。
func AllocsPerRun(n int, f func()) (avg float64)
n
: 関数f
を実行する回数。f
: メモリ割り当てを測定したい関数。- 戻り値:
f
が1回実行されるあたりの平均メモリ割り当て回数。
この関数は、メモリ割り当ての最適化を評価する際に非常に有用です。例えば、あるアルゴリズムを改善した際に、その変更がメモリ割り当てを減らしたかどうかを確認するために使用できます。
4. 並行性とメモリ割り当ての非決定性
複数のCPUコアで並行してコードが実行される場合、メモリ割り当てのタイミングや順序は非決定論的になる可能性があります。これは、複数のゴルーチンが同時にメモリを要求したり、ガベージコレクタが異なるタイミングで動作したりするためです。AllocsPerRun
のような正確な測定を目的としたテストでは、このような非決定性がテスト結果のばらつきや不安定性を引き起こす原因となります。特に、ガベージコレクタの動作は、利用可能なCPUコア数やメモリ使用状況によって大きく変動するため、GOMAXPROCS > 1
の環境では、単一のCPUコア環境よりも予測が難しくなります。
技術的詳細
このコミットは、GOMAXPROCS
が1より大きい場合にtesting.AllocsPerRun
を使用するテストをスキップするという、シンプルかつ効果的な解決策を導入しています。
なぜGOMAXPROCS > 1
でAllocsPerRun
が不安定になるのか
- ガベージコレクタの並行性: Goのガベージコレクタは並行に動作します。複数のプロセッサが利用可能な場合、GCはバックグラウンドで並行してマークフェーズなどを実行できます。これにより、メモリ割り当てのタイミングとGCの実行タイミングが複雑に絡み合い、
AllocsPerRun
が測定する割り当て回数に影響を与える可能性があります。 - メモリ割り当ての競合: 複数のゴルーチンが同時にメモリを割り当てようとすると、内部的なロックや同期メカニズムが発生します。これらの競合の度合いは、実行時のスケジューリングや他のゴルーチンの活動によって変動し、結果として
AllocsPerRun
の測定値にノイズが混入します。 - キャッシュの挙動: マルチコア環境では、CPUキャッシュの挙動も複雑になります。異なるコアが異なるメモリ領域にアクセスすることで、キャッシュミスが増加したり、キャッシュコヒーレンシのためのオーバーヘッドが発生したりする可能性があります。これもまた、メモリ割り当てのパフォーマンスに影響を与え、
AllocsPerRun
の測定にばらつきをもたらす一因となります。 - スケジューリングの非決定性: Goのスケジューラは、利用可能なP(プロセッサ、OSスレッドに相当)にゴルーチンを割り当てます。
GOMAXPROCS > 1
の場合、どのゴルーチンがどのPでいつ実行されるかは、厳密には非決定論的です。この非決定性が、メモリ割り当てのタイミングに影響を与え、AllocsPerRun
の測定結果に変動をもたらします。
解決策としてのテストスキップ
このコミットでは、これらの複雑な相互作用による不安定性を回避するために、GOMAXPROCS
が1より大きい場合にAllocsPerRun
を使用するテストをスキップするというアプローチを採用しています。
runtime.GOMAXPROCS(0)
: この関数呼び出しは、現在のGOMAXPROCS
の値を返します。引数に0を渡すことで、値を変更せずに現在の設定値を取得できます。t.Skip("skipping; GOMAXPROCS>1")
:testing.T
型のSkip
メソッドは、現在のテストをスキップし、その理由をログに出力します。これにより、テストスイート全体が失敗することなく、特定の条件下で不安定になるテストのみが除外されます。
このアプローチは、テストの信頼性を確保しつつ、開発者がGOMAXPROCS=1
の環境で引き続きメモリ割り当ての最適化を評価できるようにします。本質的に、AllocsPerRun
は単一スレッド環境でのメモリ割り当ての挙動を測定するのに最も適しているという判断がなされたと言えます。
コアとなるコードの変更箇所
このコミットでは、Go標準ライブラリ内の複数の_test.go
ファイルに同様のコードが追加されています。変更のパターンは以下の通りです。
runtime
パッケージのインポートを追加。testing.AllocsPerRun
を使用しているテスト関数の冒頭に、GOMAXPROCS
の値を確認する条件分岐を追加。
具体的には、以下のファイルが変更されています。
src/pkg/encoding/gob/timing_test.go
src/pkg/fmt/fmt_test.go
src/pkg/net/http/header_test.go
src/pkg/net/rpc/server_test.go
src/pkg/path/filepath/path_test.go
src/pkg/path/path_test.go
src/pkg/reflect/all_test.go
src/pkg/sort/search_test.go
src/pkg/strconv/strconv_test.go
src/pkg/time/time_test.go
これらのファイル内のTestCountEncodeMallocs
, TestCountDecodeMallocs
, TestCountMallocs
, TestHeaderWriteSubsetMallocs
, TestSearchWrappersDontAlloc
, noAlloc
などの関数が影響を受けています。
コアとなるコードの解説
変更のコアとなるコードは、以下のスニペットです。
import "runtime" // 追加
// ...
func TestSomeMallocs(t *testing.T) {
if runtime.GOMAXPROCS(0) > 1 {
t.Skip("skipping; GOMAXPROCS>1")
}
// ... 既存の AllocsPerRun を使用したテストロジック ...
}
-
import "runtime"
:runtime
パッケージは、Goランタイムとの相互作用を可能にする関数を提供します。ここでは、現在のGOMAXPROCS
の値を問い合わせるために必要です。 -
if runtime.GOMAXPROCS(0) > 1 { ... }
:runtime.GOMAXPROCS(0)
: この関数呼び出しは、現在のGOMAXPROCS
の値を返します。引数に0
を渡すことで、値を変更せずに現在の設定値を取得できます。> 1
: 取得したGOMAXPROCS
の値が1より大きいかどうかをチェックします。これは、Goランタイムが複数のOSスレッドを使用してゴルーチンを並行実行できる状態にあることを意味します。
-
t.Skip("skipping; GOMAXPROCS>1")
:- この条件が真(
GOMAXPROCS > 1
)の場合、testing.T
型のSkip
メソッドが呼び出されます。 Skip
メソッドは、現在のテスト関数を直ちに終了させ、テスト結果として「スキップされた」とマークします。引数として渡された文字列は、スキップの理由としてテスト出力に表示されます。- これにより、マルチコア環境での
AllocsPerRun
テストの不安定な挙動が回避され、テストスイート全体の信頼性が向上します。
- この条件が真(
この変更は、各テストファイルに個別に適用されており、AllocsPerRun
を使用するすべての関連テストがこの新しいロジックに従うようになっています。
関連リンク
- Go Change-Id:
I2222222222222222222222222222222222222222
(コミットメッセージに記載されているhttps://golang.org/cl/7545043
は、GoのコードレビューシステムであるGerritのChange-Idを示しています。これは、このコミットがGerrit上でレビューされた際の識別子です。)
参考にした情報源リンク
- Go言語の公式ドキュメント (testingパッケージ)
- Go言語の公式ドキュメント (runtimeパッケージ)
- Go言語のガベージコレクションに関する情報 (一般的なGCの概念理解のため)
- Go言語の並行処理に関する情報 (一般的な並行処理の概念理解のため)