[インデックス 12894] ファイルの概要
このコミットは、Go言語の標準ライブラリであるstrconv
パッケージにおけるメモリ確保(malloc)テストの信頼性を向上させることを目的としています。具体的には、既存のftoa_test.go
とitoa_test.go
ファイルに分散していたメモリ確保に関するテストロジックをstrconv_test.go
という新しい共通のテストファイルに集約し、より堅牢なテストフレームワークを導入することで、テスト結果の安定性を高めています。
コミット
- コミットハッシュ:
84ef97b59c89b7d9fdc04a1a8a438cd3257bf521
- 作者: Dave Cheney dave@cheney.net
- コミット日時: 2012年4月14日 土曜日 21:34:08 +1000
- コミットメッセージ:
strconv: make malloc tests more reliable Fixes #3495. I adapted fmt.TestCountMallocs to fix the existing tests. As the resulting tests did not appear to belong to either itoa or ftoa I moved them into their own file. R=bradfitz, fullung CC=golang-dev https://golang.org/cl/5985072
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/84ef97b59c89b7d9fdc04a1a8a438cd3257bf521
元コミット内容
strconv: make malloc tests more reliable
Fixes #3495.
I adapted fmt.TestCountMallocs to fix the
existing tests. As the resulting tests did not
appear to belong to either itoa or ftoa I moved
them into their own file.
R=bradfitz, fullung
CC=golang-dev
https://golang.org/cl/5985072
変更の背景
このコミットの主な背景は、strconv
パッケージ内の既存のメモリ確保テストが不安定であったことです。Go言語のコンパイラ最適化(特にエスケープ解析)やランタイムの挙動は、メモリ確保の回数に影響を与える可能性があり、単純なメモリ確保テストでは再現性の問題や誤検出が発生することがありました。
コミットメッセージにあるFixes #3495
は、この問題がGoのIssueトラッカーで報告されていたことを示唆しています。作者は、fmt
パッケージのTestCountMallocs
という既存のテスト手法を参考に、より信頼性の高いメモリ確保テストを実装する必要性を感じていました。
また、ftoa
(浮動小数点数から文字列への変換)とitoa
(整数から文字列への変換)のテストファイルにそれぞれ独立して存在していたメモリ確保テストが、どちらの機能にも直接的に属さない共通のテストロジックであったため、これらを独立したファイルに集約することで、コードの整理と再利用性を高める意図もありました。
前提知識の解説
Go言語のメモリ管理とアロケーション
Go言語は、自動メモリ管理(ガベージコレクション、GC)を採用しています。開発者はC++のように手動でメモリを解放する必要はありません。Goプログラムが実行される際、新しいオブジェクトが作成されると、そのオブジェクトはヒープ(heap)またはスタック(stack)のいずれかに割り当てられます。
- スタックアロケーション: 関数内で宣言されたローカル変数など、生存期間が短いオブジェクトは通常スタックに割り当てられます。スタックは高速で、メモリの解放も関数の終了と同時に自動的に行われます。
- ヒープアロケーション: グローバル変数、関数から返されるオブジェクト、またはコンパイラのエスケープ解析によってヒープに割り当てられると判断されたオブジェクトはヒープに割り当てられます。ヒープに割り当てられたオブジェクトは、ガベージコレクタによって管理され、不要になった時点で解放されます。ヒープアロケーションはスタックアロケーションよりもコストが高く、GCのオーバーヘッドも発生します。
Goのパフォーマンス最適化において、不必要なヒープアロケーションを減らすことは重要な要素の一つです。
テストにおけるメモリ確保の計測
Goのruntime
パッケージは、プログラムの実行時統計情報にアクセスするための機能を提供します。特にruntime.MemStats
構造体は、メモリ使用量、ガベージコレクションの統計、そしてメモリ確保(malloc)の回数など、詳細な情報を含んでいます。
runtime.ReadMemStats(m *MemStats)
: 現在のメモリ統計情報をMemStats
構造体に読み込みます。m.Mallocs
: プログラム開始以降にヒープに割り当てられたオブジェクトの総数を表します。
メモリ確保の回数をテストで計測する一般的なパターンは、テスト対象の処理を実行する前と後でruntime.MemStats
を取得し、Mallocs
の差分を計算することです。また、ガベージコレクタの動作がテスト結果に影響を与えないように、テストの前にruntime.GC()
を呼び出して強制的にGCを実行することが推奨されます。
エスケープ解析 (Escape Analysis)
エスケープ解析は、Goコンパイラが行う重要な最適化の一つです。これは、変数が関数のスコープを「エスケープ」するかどうか(つまり、関数が終了した後も参照され続ける可能性があるか)を分析し、その結果に基づいて変数をスタックに割り当てるかヒープに割り当てるかを決定します。
例えば、関数内で作成されたオブジェクトがその関数の戻り値として返される場合、そのオブジェクトは関数のスコープをエスケープするため、ヒープに割り当てられる必要があります。しかし、オブジェクトが関数内で完結し、外部に参照が漏れない場合は、スタックに割り当てることが可能です。
エスケープ解析は、不必要なヒープアロケーションを減らし、GCの負荷を軽減することで、プログラムのパフォーマンスを向上させます。しかし、この最適化の挙動はコンパイラのバージョンやコードの書き方によって変化する可能性があり、メモリ確保テストの信頼性を低下させる要因となることがあります。コミットメッセージの// TODO(bradfitz): this might be 0, once escape analysis is better
というコメントは、エスケープ解析の進化によって将来的にアロケーション数が変わる可能性を認識していることを示しています。
strconv
パッケージ
strconv
パッケージは、Go言語において基本的なデータ型(整数、浮動小数点数、真偽値など)と文字列との間で変換を行うための機能を提供します。例えば、Atoi
(文字列から整数へ)、Itoa
(整数から文字列へ)、ParseFloat
(文字列から浮動小数点数へ)、FormatFloat
(浮動小数点数から文字列へ)などの関数があります。
このコミットで言及されているAppendInt
やAppendFloat
は、既存のバイトスライスに数値を文字列として追加する関数で、メモリの再利用を促進し、アロケーションを減らすことを目的としています。
技術的詳細
このコミットの主要な技術的変更点は、メモリ確保テストのロジックを改善し、より信頼性の高い計測を可能にしたことです。
-
既存テストの削除:
src/pkg/strconv/ftoa_test.go
からTestAppendFloatDoesntAllocate
関数が削除されました。src/pkg/strconv/itoa_test.go
からnumAllocations
関数、globalBuf
変数、およびTestAppendUintDoesntAllocate
関数が削除されました。 これらのテストは、個々のファイルで独立してメモリ確保を計測していましたが、その方法が不安定であったと考えられます。
-
新しい共通テストファイルの導入:
src/pkg/strconv/strconv_test.go
という新しいファイルが作成されました。このファイルは、ftoa
やitoa
といった特定の変換機能に依存しない、strconv
パッケージ全体に共通するテストロジックを格納するために使用されます。
-
TestCountMallocs
関数の導入:- 新しい
strconv_test.go
ファイルには、TestCountMallocs
という新しいテスト関数が追加されました。この関数は、複数のメモリ確保テストケースをまとめて実行し、その信頼性を高めるための工夫が凝らされています。
- 新しい
-
mallocTest
構造体の導入:TestCountMallocs
は、mallocTest
という匿名構造体のスライスを利用して、テストケースを定義しています。各テストケースは以下の情報を含みます。count
: 期待されるメモリ確保の回数。desc
: テストケースの説明。fn
: 実際にメモリ確保を発生させる無名関数。
-
信頼性向上のための計測ロジック:
TestCountMallocs
内では、各テストケースのfn
をN
回(ここでは100回)繰り返し実行しています。- テスト実行前と実行後に
runtime.ReadMemStats
を呼び出し、Mallocs
の差分を計算します。 runtime.GC()
をテストループの前に呼び出すことで、ガベージコレクタの動作がテスト結果に与える影響を最小限に抑えています。mallocs/N > uint64(mt.count)
という条件でアサートすることで、単一の実行ではなく、複数回の実行における平均的なメモリ確保回数を検証し、テストの信頼性を向上させています。これにより、一時的なランタイムの挙動やエスケープ解析の揺らぎによる誤検出を減らすことができます。
この変更により、strconv
パッケージのメモリ確保に関するテストは、より安定し、Goランタイムやコンパイラの進化にも対応しやすい形になりました。
コアとなるコードの変更箇所
このコミットでは、以下の3つのファイルが変更されています。
-
src/pkg/strconv/ftoa_test.go
TestAppendFloatDoesntAllocate
関数が完全に削除されました。
--- a/src/pkg/strconv/ftoa_test.go +++ b/src/pkg/strconv/ftoa_test.go @@ -173,23 +173,6 @@ func TestFtoaRandom(t *testing.T) { }\n }\n \n-func TestAppendFloatDoesntAllocate(t *testing.T) { -\tn := numAllocations(func() { -\t\tvar buf [64]byte -\t\tAppendFloat(buf[:0], 1.23, 'g', 5, 64) -\t}) -\twant := 1 // TODO(bradfitz): this might be 0, once escape analysis is better -\tif n != want { -\t\tt.Errorf("with local buffer, did %d allocations, want %d", n, want) -\t} -\tn = numAllocations(func() { -\t\tAppendFloat(globalBuf[:0], 1.23, 'g', 5, 64) -\t}) -\tif n != 0 { -\t\tt.Errorf("with reused buffer, did %d allocations, want 0", n) -\t} -}\n-\n func BenchmarkFormatFloatDecimal(b *testing.B) { for i := 0; i < b.N; i++ { FormatFloat(33909, 'g', -1, 64)
-
src/pkg/strconv/itoa_test.go
runtime
パッケージのインポートが削除されました。numAllocations
関数、globalBuf
変数、およびTestAppendUintDoesntAllocate
関数が完全に削除されました。
--- a/src/pkg/strconv/itoa_test.go +++ b/src/pkg/strconv/itoa_test.go @@ -5,7 +5,6 @@ package strconv_test import ( -\t"runtime" \t. "strconv" \t"testing" ) @@ -126,35 +125,6 @@ func TestUitoa(t *testing.T) { }\n }\n \n-func numAllocations(f func()) int { -\truntime.GC() -\tmemstats := new(runtime.MemStats) -\truntime.ReadMemStats(memstats) -\tn0 := memstats.Mallocs -\tf() -\truntime.ReadMemStats(memstats) -\treturn int(memstats.Mallocs - n0) -}\n-\n-var globalBuf [64]byte -\n-func TestAppendUintDoesntAllocate(t *testing.T) { -\tn := numAllocations(func() { -\t\tvar buf [64]byte -\t\tAppendInt(buf[:0], 123, 10) -\t}) -\twant := 1 // TODO(bradfitz): this might be 0, once escape analysis is better -\tif n != want { -\t\tt.Errorf("with local buffer, did %d allocations, want %d", n, want) -\t} -\tn = numAllocations(func() { -\t\tAppendInt(globalBuf[:0], 123, 10) -\t}) -\tif n != 0 { -\t\tt.Errorf("with reused buffer, did %d allocations, want 0", n) -\t} -}\n-\n func BenchmarkFormatInt(b *testing.B) { for i := 0; i < b.N; i++ { \t\tfor _, test := range itob64tests {
-
src/pkg/strconv/strconv_test.go
- このファイルは新規作成されました。
runtime
パッケージとstrconv
パッケージ、testing
パッケージがインポートされています。globalBuf
というグローバルなバイト配列が宣言されています。mallocTest
という匿名構造体のスライスが定義され、複数のテストケースが含まれています。TestCountMallocs
関数が定義され、mallocTest
の各ケースを繰り返し実行し、メモリ確保数を検証するロジックが実装されています。
--- /dev/null +++ b/src/pkg/strconv/strconv_test.go @@ -0,0 +1,51 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package strconv_test + +import ( + "runtime" + . "strconv" + "testing" +) + +var ( + globalBuf [64]byte + + mallocTest = []struct { + count int + desc string + fn func() + }{ + // TODO(bradfitz): this might be 0, once escape analysis is better + {1, `AppendInt(localBuf[:0], 123, 10)`, func() { + var localBuf [64]byte + AppendInt(localBuf[:0], 123, 10) + }}, + {0, `AppendInt(globalBuf[:0], 123, 10)`, func() { AppendInt(globalBuf[:0], 123, 10) }}, + // TODO(bradfitz): this might be 0, once escape analysis is better + {1, `AppendFloat(localBuf[:0], 1.23, 'g', 5, 64)`, func() { + var localBuf [64]byte + AppendFloat(localBuf[:0], 1.23, 'g', 5, 64) + }}, + {0, `AppendFloat(globalBuf[:0], 1.23, 'g', 5, 64)`, func() { AppendFloat(globalBuf[:0], 1.23, 'g', 5, 64) }}, + } +) + +func TestCountMallocs(t *testing.T) { + for _, mt := range mallocTest { + const N = 100 + memstats := new(runtime.MemStats) + runtime.ReadMemStats(memstats) + mallocs := 0 - memstats.Mallocs + for i := 0; i < N; i++ { + mt.fn() + } + runtime.ReadMemStats(memstats) + mallocs += memstats.Mallocs + if mallocs/N > uint64(mt.count) { + t.Errorf("%s: expected %d mallocs, got %d", mt.desc, mt.count, mallocs/N) + } + } +}
コアとなるコードの解説
新しく追加されたsrc/pkg/strconv/strconv_test.go
ファイルが、このコミットの核心部分です。
globalBuf
変数
var globalBuf [64]byte
これは、AppendInt
やAppendFloat
関数に渡される再利用可能なバッファとして使用されます。グローバル変数として宣言することで、ヒープアロケーションを避けてスタックに割り当てられることを期待し、アロケーションが0になるべきケースのテストに使用されます。
mallocTest
構造体スライス
var (
globalBuf [64]byte
mallocTest = []struct {
count int
desc string
fn func()
}{
// TODO(bradfitz): this might be 0, once escape analysis is better
{1, `AppendInt(localBuf[:0], 123, 10)`, func() {
var localBuf [64]byte
AppendInt(localBuf[:0], 123, 10)
}},
{0, `AppendInt(globalBuf[:0], 123, 10)`, func() { AppendInt(globalBuf[:0], 123, 10) }},
// TODO(bradfitz): this might be 0, once escape analysis is better
{1, `AppendFloat(localBuf[:0], 1.23, 'g', 5, 64)`, func() {
var localBuf [64]byte
AppendFloat(localBuf[:0], 1.23, 'g', 5, 64)
}},
{0, `AppendFloat(globalBuf[:0], 1.23, 'g', 5, 64)`, func() { AppendFloat(globalBuf[:0], 1.23, 'g', 5, 64) }},
}
)
mallocTest
は、メモリ確保テストの各シナリオを定義する構造体のスライスです。
count
: このテストケースで期待される平均メモリ確保回数。desc
: テストケースの短い説明。fn
: 実際にテスト対象の操作を実行する無名関数。
注目すべきは、AppendInt
とAppendFloat
のテストケースがそれぞれ2つずつある点です。
- ローカルバッファを使用する場合:
var localBuf [64]byte
のように関数内で新しいバッファを宣言してAppendInt
やAppendFloat
に渡すケース。この場合、localBuf
自体はスタックに割り当てられますが、AppendInt
やAppendFloat
が内部で追加のメモリを必要とする場合(例えば、引数として渡されたスライスが小さすぎて拡張が必要な場合など)、ヒープアロケーションが発生する可能性があります。コメントにある// TODO(bradfitz): this might be 0, once escape analysis is better
は、エスケープ解析の改善によって将来的にこのアロケーションが0になる可能性を示唆しています。現時点では1回のアロケーションが期待されています。 - グローバルバッファを再利用する場合:
globalBuf[:0]
のように、事前に宣言されたグローバルバッファを再利用するケース。この場合、新しいバッファの作成によるアロケーションは発生しないため、期待されるアロケーション数は0です。これは、Goで効率的なコードを書く際の一般的なパターンであり、メモリの再利用によってパフォーマンスを向上させます。
TestCountMallocs
関数
func TestCountMallocs(t *testing.T) {
for _, mt := range mallocTest {
const N = 100
memstats := new(runtime.MemStats)
runtime.ReadMemStats(memstats)
mallocs := 0 - memstats.Mallocs // 開始時のMallocs数を記録
for i := 0; i < N; i++ {
mt.fn() // テスト対象の操作を実行
}
runtime.ReadMemStats(memstats)
mallocs += memstats.Mallocs // 終了時のMallocs数を加算
if mallocs/N > uint64(mt.count) {
t.Errorf("%s: expected %d mallocs, got %d", mt.desc, mt.count, mallocs/N)
}
}
}
この関数は、各mallocTest
ケースをループで処理します。
const N = 100
: 各テストケースを100回繰り返すことで、計測の信頼性を高めています。単一の実行では、GCのタイミングやスケジューリングによってアロケーション数が変動する可能性があるため、複数回の平均を取ることでより安定した結果を得られます。runtime.ReadMemStats(memstats)
: テスト対象の操作を実行する前と後でメモリ統計情報を取得します。mallocs := 0 - memstats.Mallocs
: テスト開始時点での総アロケーション数を記録します。mallocs += memstats.Mallocs
: テスト終了時点での総アロケーション数を加算することで、ループ内で発生したアロケーションの合計数を計算します。if mallocs/N > uint64(mt.count)
: 100回実行した合計アロケーション数をN
で割ることで、1回あたりの平均アロケーション数を算出します。この平均値が期待されるmt.count
を超えていないかを検証します。>
ではなく>=
ではないのは、エスケープ解析の改善などでアロケーションが減る可能性を許容するためと考えられます。
このTestCountMallocs
の導入により、strconv
パッケージのメモリ確保に関するテストは、より堅牢で信頼性の高いものとなりました。
関連リンク
- GitHub上のコミットページ: https://github.com/golang/go/commit/84ef97b59c89b7d9fdc04a1a8a438cd3257bf521
- Go言語の公式ドキュメント: https://go.dev/
- Go言語の
strconv
パッケージ: https://pkg.go.dev/strconv - Go言語の
runtime
パッケージ: https://pkg.go.dev/runtime
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のメモリ管理とガベージコレクションに関する一般的な情報源(ブログ記事、技術解説など)
- Go言語のエスケープ解析に関する一般的な情報源(ブログ記事、技術解説など)
- Go言語のテストに関する一般的な情報源