[インデックス 10627] ファイルの概要
コミット
fmt
パッケージにおける浮動小数点数のベンチマークに関するコミットです。特に、Sprintf
関数を用いた際のメモリ確保(mallocs)の回数に焦点を当てています。コミットメッセージには、Sprintf("%x")
(整数を16進数でフォーマット)が1回のmallocであるのに対し、Sprintf("%g")
(浮動小数点数を一般的な形式でフォーマット)が4回のmallocを発生させていることが示されています。これは、浮動小数点数のフォーマット処理におけるメモリ効率の改善の余地を示唆しています。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/66410bac3d01253af9e1e1cbec65f7a90b2007ec
元コミット内容
fmt: benchmark floating point.
mallocs per Sprintf("%x"): 1
mallocs per Sprintf("%g"): 4
R=golang-dev, gri
CC=golang-dev
https://golang.org/cl/5449106
変更の背景
このコミットの背景には、Go言語の標準ライブラリであるfmt
パッケージのパフォーマンス最適化、特にメモリ使用量の削減があります。fmt
パッケージは、Goプログラムにおいて文字列のフォーマットや入出力を行う上で非常に頻繁に利用されるため、その効率性はGoアプリケーション全体のパフォーマンスに大きな影響を与えます。
コミットメッセージに明記されているように、浮動小数点数のフォーマット(%g
)が整数のフォーマット(%x
)と比較して多くのメモリ確保(mallocs)を伴うことが判明しました。これは、浮動小数点数のフォーマット処理が内部的に追加のバッファやデータ構造を必要としている可能性を示唆しています。開発者は、このメモリ確保のオーバーヘッドを特定し、将来的な最適化の機会を探るために、浮動小数点数フォーマットのベンチマークとメモリ確保回数の計測を追加しました。
このようなベンチマークの追加は、Go言語の設計哲学である「シンプルさ」と「効率性」に基づいています。特に、ガベージコレクション(GC)のオーバーヘッドを最小限に抑えるためには、不要なメモリ確保を減らすことが重要です。このコミットは、具体的な数値(%g
が4 mallocs)を提示することで、今後の改善目標を明確にしています。
前提知識の解説
Go言語のfmt
パッケージ
fmt
パッケージは、Go言語におけるフォーマット済みI/O(入出力)を実装するための標準ライブラリです。C言語のprintf
やscanf
に似た機能を提供し、様々なデータ型を文字列に変換したり、文字列からデータを解析したりするために使用されます。
Sprintf
:fmt.Sprintf(format string, a ...interface{}) string
は、指定されたフォーマット文字列と引数を使用して文字列を生成し、その結果の文字列を返します。ファイルや標準出力には書き込みません。Printf
:fmt.Printf(format string, a ...interface{}) (n int, err error)
は、フォーマット済み文字列を標準出力に書き込みます。Fprintf
:fmt.Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error)
は、指定されたio.Writer
(例:bytes.Buffer
、ファイル)にフォーマット済み文字列を書き込みます。
メモリ確保(mallocs)とガベージコレクション(GC)
Go言語はガベージコレクタ(GC)を備えており、開発者が手動でメモリを解放する必要はありません。しかし、GCはプログラムの実行中に一時停止(Stop-the-World)を引き起こす可能性があり、これがアプリケーションのレイテンシに影響を与えることがあります。メモリ確保(mallocs)の回数が多いほど、GCが頻繁に実行される可能性が高まり、パフォーマンスが低下する可能性があります。
このため、Goのパフォーマンス最適化においては、不要なメモリ確保を減らすことが重要な戦略の一つとなります。特に、ループ内で頻繁に呼び出される関数や、大量のデータ処理を行う部分では、メモリ確保の回数を最小限に抑えることが求められます。
runtime.MemStats
runtime.MemStats
は、Goプログラムのメモリ使用状況に関する統計情報を提供する構造体です。runtime.ReadMemStats
関数を呼び出すことで、この構造体に現在のメモリ統計が格納されます。
runtime.MemStats.Mallocs
: プログラムが開始されてから、または最後にruntime.GC()
が呼び出されてから、ヒープ上で確保されたオブジェクトの総数を示します。このコミットでは、特定の処理の前後にこの値を計測し、その差分を取ることで、その処理中に発生したメモリ確保の回数を算出しています。
Go言語のベンチマークテスト
Go言語には、標準でベンチマークテストを記述するためのフレームワークが組み込まれています。testing
パッケージを使用し、関数名をBenchmarkXxx
とすることでベンチマーク関数として認識されます。
testing.B
: ベンチマーク関数に渡される構造体で、ベンチマークの実行を制御するためのメソッドを提供します。b.N
: ベンチマーク関数が実行されるイテレーション回数を示します。Goのテストフレームワークが自動的に適切なb.N
の値を決定し、統計的に有意な結果が得られるように調整します。ベンチマーク関数は、このb.N
回だけテスト対象のコードを実行する必要があります。
浮動小数点数フォーマット(%g
)
fmt
パッケージにおける%g
動詞は、浮動小数点数を「より短く、より読みやすい」形式で表示するために使用されます。これは、数値の大きさに応じて指数表記(%e
)と通常の表記(%f
)を自動的に切り替えます。例えば、1.23456e+06
のような大きな数値や、0.00000123456
のような小さな数値では指数表記が選ばれ、それ以外の数値では通常の表記が選ばれます。この自動的な切り替えは、内部的に数値の解析や文字列変換のロジックが複雑になる可能性があり、それが追加のメモリ確保につながる一因となることがあります。
技術的詳細
このコミットは、Goのfmt
パッケージにおけるメモリ確保の効率性を評価するためのテストコードの追加と修正に焦点を当てています。特に、浮動小数点数のフォーマット処理が他のデータ型と比較してどの程度のメモリを消費するかを定量的に測定しようとしています。
TestCountMallocs
関数は、runtime.MemStats
を利用して、特定のSprintf
呼び出しが平均して何回のメモリ確保(mallocs)を引き起こすかを計測しています。このテストは、runtime.UpdateMemStats()
を呼び出して現在のメモリ統計を更新し、その前後のMallocs
カウンタの差分を取ることで、対象の処理中に発生したmallocsの数を算出します。
変更点として、TestCountMallocs
関数内でN
という定数(100)を導入し、ループ回数を明示的に定義しています。これにより、各Sprintf
呼び出しの平均mallocs数をより正確に計算できるようになります。
最も重要な追加は、浮動小数点数フォーマット(Sprintf("%g", ...)
)のmallocs計測です。コミットメッセージで示されているように、Sprintf("%g")
が4回のmallocを発生させているという結果は、この処理が他のシンプルなフォーマット(例: Sprintf("%x")
の1回)と比較して、より多くの内部的なメモリ割り当てを必要としていることを示しています。これは、浮動小数点数の文字列変換が、数値の精度、指数表記への切り替え、丸め処理など、より複雑なロジックを伴うためと考えられます。これらの処理には、一時的なバッファや中間結果を格納するためのメモリが必要となる場合があります。
また、BenchmarkSprintfFloat
という新しいベンチマーク関数が追加されています。これは、Sprintf("%g", 5.23184)
という特定の浮動小数点数フォーマット操作の実行時間を計測することを目的としています。このベンチマークは、メモリ確保の回数だけでなく、実際の実行速度の観点からも浮動小数点数フォーマットのパフォーマンスを評価するために使用されます。
これらのテストとベンチマークの追加は、Goのfmt
パッケージのパフォーマンス特性を深く理解し、将来的な最適化のための具体的なデータポイントを提供することを目的としています。特に、メモリ確保の回数を減らすことは、ガベージコレクションの頻度を減らし、結果としてアプリケーションの全体的なパフォーマンスとレイテンシを向上させる上で非常に重要です。
コアとなるコードの変更箇所
変更はすべて src/pkg/fmt/fmt_test.go
ファイル内で行われています。
--- a/src/pkg/fmt/fmt_test.go
+++ b/src/pkg/fmt/fmt_test.go
@@ -500,69 +500,84 @@ func BenchmarkSprintfPrefixedInt(b *testing.B) {
}\n}\n\n+func BenchmarkSprintfFloat(b *testing.B) {\n+\tfor i := 0; i < b.N; i++ {\n+\t\tSprintf("%g", 5.23184)\n+\t}\n+}\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 < 100; i++ {\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/100)\n+\tPrintf("mallocs per Sprintf(\\\"\\\"): %d\\n", mallocs/N)\n \truntime.UpdateMemStats()\n \tmallocs = 0 - runtime.MemStats.Mallocs\n-\tfor i := 0; i < 100; i++ {\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/100)\n+\tPrintf("mallocs per Sprintf(\\\"xxx\\\"): %d\\n", mallocs/N)\n \truntime.UpdateMemStats()\n \tmallocs = 0 - runtime.MemStats.Mallocs\n-\tfor i := 0; i < 100; i++ {\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/100)\n+\tPrintf("mallocs per Sprintf(\\\"%%x\\\"): %d\\n", mallocs/N)\n \truntime.UpdateMemStats()\n \tmallocs = 0 - runtime.MemStats.Mallocs\n-\tfor i := 0; i < 100; i++ {\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/100)\n+\tPrintf("mallocs per Sprintf(\\\"%%s\\\"): %d\\n", mallocs/N)\n \truntime.UpdateMemStats()\n \tmallocs = 0 - runtime.MemStats.Mallocs\n-\tfor i := 0; i < 100; i++ {\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/100)\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 < 100; i++ {\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/100)\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 < 100; i++ {\n+\tfor i := 0; i < N; i++ {\n \t\tbuf.Reset()\n \t\tFprintf(buf, "%s", "hello")\n \t}\n \truntime.UpdateMemStats()\n \tmallocs += runtime.MemStats.Mallocs\n-\tPrintf("mallocs per Fprintf(buf, \\\"%%s\\\"): %d\\n", mallocs/100)\n+\tPrintf("mallocs per Fprintf(buf, \\\"%%s\\\"): %d\\n", mallocs/N)\n }\n \n type flagPrinter struct{}\n```
## コアとなるコードの解説
### `BenchmarkSprintfFloat`関数の追加
```go
func BenchmarkSprintfFloat(b *testing.B) {
for i := 0; i < b.N; i++ {
Sprintf("%g", 5.23184)
}
}
この関数は、fmt.Sprintf
を使用して浮動小数点数(5.23184
)を%g
フォーマットで文字列に変換する処理のベンチマークを行います。b.N
はベンチマークフレームワークによって決定される繰り返し回数で、このループ内でSprintf
が繰り返し呼び出され、その実行時間が計測されます。これにより、浮動小数点数フォーマットのパフォーマンス特性を定量的に評価できるようになります。
TestCountMallocs
関数の変更
-
定数
N
の導入:const N = 100
以前はハードコードされていたループ回数
100
をN
という定数に置き換えました。これにより、コードの可読性が向上し、将来的にループ回数を変更する際のメンテナンスが容易になります。 -
ループ回数の変更: 既存のすべての
for
ループの100
がN
に置き換えられました。- for i := 0; i < 100; i++ { + for i := 0; i < N; i++ {
そして、
Printf
文の除算も100
からN
に変更されました。- Printf("mallocs per Sprintf(\\\"\\\"): %d\\n", mallocs/100) + Printf("mallocs per Sprintf(\\\"\\\"): %d\\n", mallocs/N)
これは、各
Sprintf
呼び出しあたりの平均mallocs数を正確に計算するための変更です。 -
浮動小数点数フォーマットのmallocs計測の追加:
runtime.UpdateMemStats() mallocs = 0 - runtime.MemStats.Mallocs for i := 0; i < N; i++ { Sprintf("%g", 3.14159) } runtime.UpdateMemStats() mallocs += runtime.MemStats.Mallocs Printf("mallocs per Sprintf(\\\"%%g\\\"): %d\\n", mallocs/N)
このブロックが新たに追加されました。これは、
Sprintf("%g", 3.14159)
という浮動小数点数フォーマット操作が何回のメモリ確保を引き起こすかを計測します。- まず、
runtime.UpdateMemStats()
を呼び出して現在のメモリ統計を更新します。 - 次に、
mallocs
変数に現在のruntime.MemStats.Mallocs
の負の値を格納し、計測開始時点のmallocs数を記録します。 N
回ループでSprintf("%g", 3.14159)
を実行します。- 再度
runtime.UpdateMemStats()
を呼び出してメモリ統計を更新します。 mallocs
変数に現在のruntime.MemStats.Mallocs
の値を加算することで、ループ中に発生したmallocsの総数を算出します。- 最後に、
mallocs/N
として1回のSprintf("%g")
呼び出しあたりの平均mallocs数を標準出力に表示します。
- まず、
これらの変更により、Goのfmt
パッケージにおける浮動小数点数フォーマットのメモリ使用特性が明確に測定できるようになり、将来的なパフォーマンス改善のための重要なデータが提供されます。
関連リンク
- Go Gerrit Change-ID: https://golang.org/cl/5449106
参考にした情報源リンク
- Go言語
fmt
パッケージのドキュメント: https://pkg.go.dev/fmt - Go言語
testing
パッケージのドキュメント: https://pkg.go.dev/testing - Go言語
runtime
パッケージのドキュメント: https://pkg.go.dev/runtime - Go言語のベンチマークに関する公式ブログ記事やドキュメント (一般的な情報源として):
- "Go's
testing
package": https://go.dev/blog/testing - "Profiling Go Programs": https://go.dev/blog/pprof
- "Go's
- ガベージコレクションとメモリ管理に関するGo言語のドキュメント (一般的な情報源として):
- "Go's Memory Model": https://go.dev/ref/mem
- "Go's runtime and garbage collection": https://go.dev/doc/diagnostics
- "Go: The Good, Bad, and Ugly Parts of Memory Management": https://www.ardanlabs.com/blog/2018/12/go-memory-management-good-bad-ugly.html (Ardan Labsのブログ記事はGoのメモリ管理について深く掘り下げています)
- 浮動小数点数フォーマットに関する一般的な情報 (IEEE 754など): https://ja.wikipedia.org/wiki/%E6%B5%AE%E5%8B%95%E5%B0%8F%E6%95%B0%E7%82%B9%E6%95%B0