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

[インデックス 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言語のprintfscanfに似た機能を提供し、様々なデータ型を文字列に変換したり、文字列からデータを解析したりするために使用されます。

  • 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関数の変更

  1. 定数Nの導入:

    const N = 100
    

    以前はハードコードされていたループ回数100Nという定数に置き換えました。これにより、コードの可読性が向上し、将来的にループ回数を変更する際のメンテナンスが容易になります。

  2. ループ回数の変更: 既存のすべてのforループの100Nに置き換えられました。

    -	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数を正確に計算するための変更です。

  3. 浮動小数点数フォーマットの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パッケージにおける浮動小数点数フォーマットのメモリ使用特性が明確に測定できるようになり、将来的なパフォーマンス改善のための重要なデータが提供されます。

関連リンク

参考にした情報源リンク