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

[インデックス 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 > 1AllocsPerRunが不安定になるのか

  1. ガベージコレクタの並行性: Goのガベージコレクタは並行に動作します。複数のプロセッサが利用可能な場合、GCはバックグラウンドで並行してマークフェーズなどを実行できます。これにより、メモリ割り当てのタイミングとGCの実行タイミングが複雑に絡み合い、AllocsPerRunが測定する割り当て回数に影響を与える可能性があります。
  2. メモリ割り当ての競合: 複数のゴルーチンが同時にメモリを割り当てようとすると、内部的なロックや同期メカニズムが発生します。これらの競合の度合いは、実行時のスケジューリングや他のゴルーチンの活動によって変動し、結果としてAllocsPerRunの測定値にノイズが混入します。
  3. キャッシュの挙動: マルチコア環境では、CPUキャッシュの挙動も複雑になります。異なるコアが異なるメモリ領域にアクセスすることで、キャッシュミスが増加したり、キャッシュコヒーレンシのためのオーバーヘッドが発生したりする可能性があります。これもまた、メモリ割り当てのパフォーマンスに影響を与え、AllocsPerRunの測定にばらつきをもたらす一因となります。
  4. スケジューリングの非決定性: 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ファイルに同様のコードが追加されています。変更のパターンは以下の通りです。

  1. runtimeパッケージのインポートを追加。
  2. 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 を使用したテストロジック ...
}
  1. import "runtime": runtimeパッケージは、Goランタイムとの相互作用を可能にする関数を提供します。ここでは、現在のGOMAXPROCSの値を問い合わせるために必要です。

  2. if runtime.GOMAXPROCS(0) > 1 { ... }:

    • runtime.GOMAXPROCS(0): この関数呼び出しは、現在のGOMAXPROCSの値を返します。引数に0を渡すことで、値を変更せずに現在の設定値を取得できます。
    • > 1: 取得したGOMAXPROCSの値が1より大きいかどうかをチェックします。これは、Goランタイムが複数のOSスレッドを使用してゴルーチンを並行実行できる状態にあることを意味します。
  3. 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上でレビューされた際の識別子です。)

参考にした情報源リンク