[インデックス 10984] ファイルの概要
このコミットは、Go言語の標準ライブラリであるfmt
パッケージのテストコード、具体的にはfmt_test.go
ファイル内のメモリ割り当て(malloc)テストを改善するものです。以前は手動でメモリ割り当て数を計測し出力していたテストを、期待される割り当て数を明示的にチェックする、より堅牢なテストに作り変えています。特に、浮動小数点数フォーマット指定子%g
のメモリ割り当てが最適化され、その変更がテストによって検証されるようになりました。
コミット
commit 07db252222253ac103ff46ed85a1cccc1f33b73d
Author: Rob Pike <r@golang.org>
Date: Thu Dec 22 15:16:06 2011 -0800
fmt: make the malloc test check its counts
Discover than %g is now down to 1 malloc from 2 from 4.
Have fun with funcs.
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5504077
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/07db252222253ac103ff46ed85a1cccc1f33b73d
元コミット内容
このコミットの目的は、fmt
パッケージのメモリ割り当てテストを、単に割り当て数を表示するだけでなく、その数を検証するように変更することです。これにより、%g
フォーマット指定子におけるメモリ割り当てが、以前の4回、2回から1回に削減されたことを発見し、その最適化をテストで確認できるようになりました。また、関数リテラルを活用してテストの構造を改善しています。
変更の背景
Go言語の標準ライブラリは、パフォーマンスと効率性を重視して開発されています。fmt
パッケージは、文字列のフォーマットと出力において非常に頻繁に使用されるため、そのメモリ効率は重要です。
このコミット以前のfmt
パッケージのメモリ割り当てテスト(TestCountMallocs
)は、各Sprintf
やFprintf
の呼び出しがどれくらいのメモリ割り当てを行うかを計測し、その結果を標準出力に表示する形式でした。これは情報提供には役立ちますが、以下のような課題がありました。
- 自動検証の欠如: 計測されたメモリ割り当て数が期待値と一致するかどうかを自動的に検証する仕組みがありませんでした。そのため、メモリ割り当ての回数が意図せず増加しても、テストが失敗することはありませんでした。
- 回帰の検出困難: パフォーマンス最適化によってメモリ割り当てが削減された場合、その効果を自動的に確認することが難しく、また将来の変更によってメモリ割り当てが増加する「回帰」が発生しても、それを早期に検出できませんでした。
- テストの可読性と保守性: 各テストケースが個別に記述されており、コードが冗長で、新しいテストケースを追加する際に手間がかかりました。
このコミットは、これらの課題を解決し、fmt
パッケージのメモリ効率に関するテストをより堅牢で自動化されたものにすることを目的としています。特に、%g
フォーマット指定子におけるメモリ割り当ての最適化(4回、2回から1回への削減)が実際に達成され、それがテストによって保証されるようになったことが強調されています。
前提知識の解説
Go言語のfmt
パッケージ
fmt
パッケージは、Go言語における基本的なI/Oフォーマット機能を提供します。C言語のprintf
やscanf
に似た関数群を持ち、文字列、数値、構造体などの値を整形して出力したり、文字列から値を読み取ったりするために使用されます。
fmt.Sprintf
: フォーマットされた文字列を生成し、その文字列を返します。fmt.Fprintf
: フォーマットされた文字列を指定されたio.Writer
(例:bytes.Buffer
、ファイル、ネットワーク接続など)に書き込みます。- フォーマット指定子:
%d
(整数)、%s
(文字列)、%f
(浮動小数点数)、%g
(浮動小数点数、%e
または%f
の短い方)、%x
(16進数)など、様々なデータ型に対応する指定子があります。
メモリ割り当て (malloc) とGoのガベージコレクション
プログラムが実行中に動的にメモリを確保する操作を「メモリ割り当て」または「アロケーション」と呼びます。Go言語では、new
やmake
、あるいはスライスやマップの追加など、様々な操作でメモリ割り当てが発生します。Goにはガベージコレクタ(GC)が組み込まれており、不要になったメモリ領域を自動的に解放します。
メモリ割り当て自体は必要な操作ですが、過度な割り当てはGCの負荷を増やし、プログラムのパフォーマンスに影響を与える可能性があります。そのため、特にパフォーマンスが重視されるライブラリやアプリケーションでは、メモリ割り当ての回数を最小限に抑えることが最適化の一環として行われます。
Goのtesting
パッケージ
Go言語には、標準でテストフレームワークが提供されており、testing
パッケージを通じて利用できます。
- テスト関数:
Test
で始まる関数(例:func TestCountMallocs(t *testing.T)
)は、Goのテストツールによって自動的に実行されます。*testing.T
はテストの状態管理やエラー報告に使用されます。 - ベンチマーク関数:
Benchmark
で始まる関数(例:func BenchmarkSprintfFloat(b *testing.B)
)は、コードのパフォーマンスを計測するために使用されます。 t.Errorf
: テスト中にエラーが発生した場合に、エラーメッセージを出力し、テストを失敗としてマークするために使用されます。
runtime
パッケージとruntime.MemStats
runtime
パッケージは、Goランタイムとのインタラクションを可能にする低レベルな機能を提供します。
runtime.MemStats
: Goプログラムのメモリ使用状況に関する統計情報を含む構造体です。Mallocs
: これまでに割り当てられたオブジェクトの総数(解放されたものも含む)を表すuint64
型のフィールドです。
runtime.UpdateMemStats()
:MemStats
構造体の情報を最新の状態に更新します。メモリ統計情報を取得する前にこの関数を呼び出す必要があります。
bytes.Buffer
bytes.Buffer
は、可変長のバイトバッファを実装する型です。io.Writer
インターフェースを満たすため、fmt.Fprintf
などの関数で出力先として利用できます。メモリ上で効率的にバイトデータを構築・操作するのに便利です。
技術的詳細
このコミットの主要な変更点は、src/pkg/fmt/fmt_test.go
ファイル内のTestCountMallocs
関数の実装方法です。
変更前:
変更前のTestCountMallocs
関数は、以下のようなパターンで各fmt
関数のメモリ割り当て数を計測していました。
runtime.UpdateMemStats()
を呼び出し、現在のMallocs
数を取得。- 対象の
fmt
関数(例:Sprintf("")
)をN
回(100回)ループで実行。 - 再度
runtime.UpdateMemStats()
を呼び出し、ループ後のMallocs
数を取得。 - 前後の
Mallocs
数の差分を計算し、N
で割って1回あたりの平均malloc数を算出。 - その結果を
Printf
で標準出力に出力。
この方法は、メモリ割り当て数を「観測」するものであり、その数が特定の期待値と一致するかどうかを「検証」するものではありませんでした。
変更後:
変更後のTestCountMallocs
関数は、より構造化されたアプローチを採用しています。
-
mallocTest
構造体スライスの導入:mallocTest
という名前のグローバル変数として、匿名構造体のスライスが定義されました。このスライスは、各テストケースの情報を保持します。var mallocTest = []struct { count int desc string fn func() }{ {0, `Sprintf("")`, func() { Sprintf("") }}, {1, `Sprintf("xxx")`, func() { Sprintf("xxx") }}, // ... 他のテストケース ... {1, `Sprintf("%g")`, func() { Sprintf("%g", 3.14159) }}, // ... }
count
: そのテストケースで期待されるメモリ割り当ての回数。desc
: テストケースの説明文字列。fn
: 実際にfmt
関数を呼び出す無名関数(関数リテラル)。これにより、各テストケースの実行ロジックがカプセル化されます。
-
テストループと自動検証:
TestCountMallocs
関数内では、mallocTest
スライスをループで反復処理します。各テストケースmt
に対して、以下の処理が行われます。runtime.UpdateMemStats()
を呼び出し、テスト実行前のMallocs
数を記録。mt.fn()
をN
回(100回)ループで実行。- 再度
runtime.UpdateMemStats()
を呼び出し、テスト実行後のMallocs
数を記録。 - 計測された総malloc数から1回あたりの平均malloc数を計算 (
mallocs/N
)。 - この平均malloc数が、
mt.count
(期待されるmalloc数)と一致するかどうかをif
文でチェックします。 - もし一致しない場合、
t.Errorf
を呼び出してエラーメッセージを出力し、テストを失敗させます。エラーメッセージには、どのテストケースで、期待値と実際の値がどう異なったかが含まれます。
この変更により、fmt
パッケージのメモリ割り当てに関するテストは、単なる情報表示から、具体的な期待値を検証する回帰テストへと進化しました。特に、コミットメッセージで言及されているように、%g
フォーマット指定子におけるメモリ割り当ての最適化(4回、2回から1回への削減)が、この新しいテスト構造によって明確に検証されるようになりました。
コアとなるコードの変更箇所
変更はsrc/pkg/fmt/fmt_test.go
ファイルに集中しています。
--- a/src/pkg/fmt/fmt_test.go
+++ b/src/pkg/fmt/fmt_test.go
@@ -506,78 +506,42 @@ func BenchmarkSprintfFloat(b *testing.B) {
}\n }\n \n+var mallocBuf bytes.Buffer\n+\n+var mallocTest = []struct {\n+\tcount int\n+\tdesc string\n+\tfn func()\n+}{\n+\t{0, `Sprintf("")`, func() { Sprintf("") }},\n+\t{1, `Sprintf("xxx")`, func() { Sprintf("xxx") }},\n+\t{1, `Sprintf("%x")`, func() { Sprintf("%x", 7) }},\n+\t{2, `Sprintf("%s")`, func() { Sprintf("%s", "hello") }},\n+\t{1, `Sprintf("%x %x")`, func() { Sprintf("%x", 7, 112) }},\n+\t{1, `Sprintf("%g")`, func() { Sprintf("%g", 3.14159) }},\n+\t{0, `Fprintf(buf, "%x %x %x")`, func() { mallocBuf.Reset(); Fprintf(&mallocBuf, "%x %x %x", 7, 8, 9) }},\n+\t{1, `Fprintf(buf, "%s")`, func() { mallocBuf.Reset(); Fprintf(&mallocBuf, "%s", "hello") }},\n+}\n+\n+var _ bytes.Buffer\n+\n func TestCountMallocs(t *testing.T) {\n \tif testing.Short() {\n \t\treturn\n \t}\n-\tconst N = 100\n-\truntime.UpdateMemStats()\n-\tmallocs := 0 - runtime.MemStats.Mallocs\n-\tfor i := 0; i < N; i++ {\n-\t\tSprintf("")\n-\t}\n-\truntime.UpdateMemStats()\n-\tmallocs += runtime.MemStats.Mallocs\n-\tPrintf("mallocs per Sprintf(\"\\\"\"): %d\\n", mallocs/N)\n-\truntime.UpdateMemStats()\n-\tmallocs = 0 - runtime.MemStats.Mallocs\n-\tfor i := 0; i < N; i++ {\n-\t\tSprintf("xxx")\n-\t}\n-\truntime.UpdateMemStats()\n-\tmallocs += runtime.MemStats.Mallocs\n-\tPrintf("mallocs per Sprintf(\"\\\"xxx\\\"\"): %d\\n", mallocs/N)\n-\truntime.UpdateMemStats()\n-\tmallocs = 0 - runtime.MemStats.Mallocs\n-\tfor i := 0; i < N; i++ {\n-\t\tSprintf("%x", i)\n-\t}\n-\truntime.UpdateMemStats()\n-\tmallocs += runtime.MemStats.Mallocs\n-\tPrintf("mallocs per Sprintf(\"%%x\"): %d\\n", mallocs/N)\n-\truntime.UpdateMemStats()\n-\tmallocs = 0 - runtime.MemStats.Mallocs\n-\tfor i := 0; i < N; i++ {\n-\t\tSprintf("%s", "hello")\n-\t}\n-\truntime.UpdateMemStats()\n-\tmallocs += runtime.MemStats.Mallocs\n-\tPrintf("mallocs per Sprintf(\"%%s\"): %d\\n", mallocs/N)\n-\truntime.UpdateMemStats()\n-\tmallocs = 0 - runtime.MemStats.Mallocs\n-\tfor i := 0; i < N; i++ {\n-\t\tSprintf("%x %x", i, i)\n-\t}\n-\truntime.UpdateMemStats()\n-\tmallocs += runtime.MemStats.Mallocs\n-\tPrintf("mallocs per Sprintf(\"%%x %%x\"): %d\\n", mallocs/N)\n-\truntime.UpdateMemStats()\n-\tmallocs = 0 - runtime.MemStats.Mallocs\n-\tfor i := 0; i < N; i++ {\n-\t\tSprintf("%g", 3.14159)\n-\t}\n-\truntime.UpdateMemStats()\n-\tmallocs += runtime.MemStats.Mallocs\n-\tPrintf("mallocs per Sprintf(\"%%g\"): %d\\n", mallocs/N)\n-\tbuf := new(bytes.Buffer)\n-\truntime.UpdateMemStats()\n-\tmallocs = 0 - runtime.MemStats.Mallocs\n-\tfor i := 0; i < N; i++ {\n-\t\tbuf.Reset()\n-\t\tFprintf(buf, "%x %x %x", i, i, i)\n-\t}\n-\truntime.UpdateMemStats()\n-\tmallocs += runtime.MemStats.Mallocs\n-\tPrintf("mallocs per Fprintf(buf, \"%%x %%x %%x\"): %d\\n", mallocs/N)\n-\truntime.UpdateMemStats()\n-\tmallocs = 0 - runtime.MemStats.Mallocs\n-\tfor i := 0; i < N; i++ {\n-\t\tbuf.Reset()\n-\t\tFprintf(buf, "%s", "hello")\n+\tfor _, mt := range mallocTest {\n+\t\tconst N = 100\n+\t\truntime.UpdateMemStats()\n+\t\tmallocs := 0 - runtime.MemStats.Mallocs\n+\t\tfor i := 0; i < N; i++ {\n+\t\t\tmt.fn()\n+\t\t}\n+\t\truntime.UpdateMemStats()\n+\t\tmallocs += runtime.MemStats.Mallocs\n+\t\tif mallocs/N != uint64(mt.count) {\n+\t\t\tt.Errorf("%s: expected %d mallocs, got %d", mt.desc, mt.count, mallocs/N)\n+\t\t}\n \t}\n-\truntime.UpdateMemStats()\n-\tmallocs += runtime.MemStats.Mallocs\n-\tPrintf("mallocs per Fprintf(buf, \"%%s\"): %d\\n", mallocs/N)\n }\n \n type flagPrinter struct{}\n```
## コアとなるコードの解説
このコミットの核心は、`TestCountMallocs`関数の再構築にあります。
1. **`mallocBuf bytes.Buffer`の追加**: `Fprintf`のテストケースで使用するための`bytes.Buffer`インスタンスが追加されました。
2. **`mallocTest`スライスの定義**:
このスライスは、各テストシナリオをカプセル化します。
* `count`: そのシナリオで期待されるメモリ割り当ての回数。例えば、`Sprintf("")`は0回の割り当て、`Sprintf("xxx")`は1回の割り当てを期待しています。
* `desc`: テストケースの短い説明。
* `fn`: 実際に`fmt`パッケージの関数(`Sprintf`や`Fprintf`)を呼び出す無名関数。これにより、テストロジックが簡潔に記述され、各テストケースが独立して実行できるようになります。特に注目すべきは、`Sprintf("%g", 3.14159)`のケースで期待される`count`が`1`になっている点です。これは、コミットメッセージで言及されている`%g`の最適化を反映しています。
3. **`TestCountMallocs`関数のループ処理**:
`TestCountMallocs`関数は、`mallocTest`スライスの各要素を反復処理します。
* 各テストケース`mt`について、`N`回(100回)のループ内で`mt.fn()`を実行します。
* ループの前後で`runtime.UpdateMemStats()`を呼び出し、`runtime.MemStats.Mallocs`の差分を取ることで、`N`回の実行における総メモリ割り当て数を計測します。
* `mallocs/N`で1回あたりの平均メモリ割り当て数を計算します。
* 最後に、`if mallocs/N != uint64(mt.count)`という条件で、計測された平均割り当て数が`mallocTest`で定義された期待値`mt.count`と一致するかどうかを検証します。
* もし一致しない場合、`t.Errorf`を呼び出してエラーを報告します。これにより、期待されるメモリ割り当て数と実際の割り当て数が異なる場合に、テストが自動的に失敗するようになります。
この変更により、`fmt`パッケージのメモリ割り当てに関するテストは、単なる情報出力から、具体的なパフォーマンス特性を保証する回帰テストへと進化しました。これにより、将来のコード変更が意図せずメモリ割り当てを増加させてしまうような回帰バグを早期に発見できるようになります。
## 関連リンク
* **Gerrit Code Review**: [https://golang.org/cl/5504077](https://golang.org/cl/5504077)
## 参考にした情報源リンク
* Go言語公式ドキュメント: `fmt`パッケージ: [https://pkg.go.dev/fmt](https://pkg.go.dev/fmt)
* Go言語公式ドキュメント: `testing`パッケージ: [https://pkg.go.dev/testing](https://pkg.go.dev/testing)
* Go言語公式ドキュメント: `runtime`パッケージ: [https://pkg.go.dev/runtime](https://pkg.go.dev/runtime)
* Go言語公式ドキュメント: `bytes`パッケージ: [https://pkg.go.dev/bytes](https://pkg.go.dev/bytes)
* Goにおけるメモリ管理とガベージコレクションに関する一般的な情報 (例: "Go Memory Management" で検索)