[インデックス 11217] ファイルの概要
このコミットは、Go言語の標準ライブラリfmt
パッケージ内のメモリ割り当てテストに関する修正です。具体的には、Sprintf
関数が浮動小数点値をinterface{}
として受け取る際に発生する不要なメモリ割り当てを回避するためのテストケースの調整が行われています。
コミット
commit b7ec659b54951f2461381ec0a5d4e71cb0460a03
Author: Rob Pike <r@golang.org>
Date: Tue Jan 17 15:42:02 2012 -0800
fmt: fix Malloc test
We need to avoid allocating an extra word for the interface value
passing the floating-point value as an interface{}. It's easy.
Fixes #2722.
R=golang-dev, gri
CC=golang-dev
https://golang.org/cl/5553044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b7ec659b54951f2461381ec0a5d4e71cb0460a03
元コミット内容
fmt: fix Malloc test
We need to avoid allocating an extra word for the interface value
passing the floating-point value as an interface{}. It's easy.
Fixes #2722.
R=golang-dev, gri
CC=golang-dev
https://golang.org/cl/5553044
変更の背景
このコミットの背景には、Go言語のfmt
パッケージにおけるメモリ割り当ての最適化と、それに関連するテストの正確性の問題があります。特に、浮動小数点数(float64
)をinterface{}
型として関数に渡す際に、Goの内部的な挙動により、値がポインタサイズに収まらない場合にヒープ上に余分なワード(メモリ領域)が割り当てられる問題がありました。
fmt.Sprintf
のような関数は、様々な型の引数を受け取るためにinterface{}
を使用します。しかし、float64
のようなプリミティブ型であっても、interface{}
に変換される際には、その値が直接インターフェースの値に埋め込まれるか、あるいはヒープに割り当てられてポインタがインターフェースの値に格納されるかがGoのランタイムによって決定されます。float64
は通常、ポインタサイズ(32bitシステムでは4バイト、64bitシステムでは8バイト)と同じかそれ以上のサイズを持つため、interface{}
に変換される際にヒープ割り当てが発生する可能性がありました。
fmt
パッケージのメモリ割り当てテスト(TestCountMallocs
)は、特定のSprintf
やFprintf
の呼び出しが期待されるメモリ割り当て数を超えていないかを検証するものです。以前のテストでは、Sprintf("%g", 3.14159)
のようにfloat64
を直接渡していたため、この余分なメモリ割り当てが発生し、テストが期待通りの結果(割り当て数1)にならず、2つの割り当てが発生していました。コミットメッセージにある「TODO: should be 1. See Issue 2722.」というコメントが、この問題を示唆しています。
このコミットは、このテストの不正確さを修正し、float64
をinterface{}
として渡す際のメモリ割り当ての挙動をより正確に反映させることを目的としています。
前提知識の解説
Go言語のインターフェースとメモリ割り当て
Go言語のインターフェースは、動的な型付けを可能にする強力な機能です。インターフェースの値は、内部的に「型」と「値」のペアとして表現されます。
- 型 (Type): インターフェースに格納されている具体的な値の型情報(例:
int
,string
,*MyStruct
など)。 - 値 (Value): インターフェースに格納されている具体的な値。
この「値」の格納方法には2つのパターンがあります。
- 直接格納 (Direct Storage): 値がポインタサイズ(通常は8バイト)に収まる場合、その値はインターフェースの値のフィールドに直接格納されます。例えば、
int
,bool
,float32
などのプリミティブ型や、小さな構造体などがこれに該当します。この場合、ヒープ割り当ては発生しません。 - 間接格納 (Indirect Storage): 値がポインタサイズを超える場合、その値はヒープに割り当てられ、インターフェースの値のフィールドにはそのヒープ上の値へのポインタが格納されます。例えば、大きな構造体、配列、スライス、マップ、チャネル、そして
float64
などがこれに該当します。この場合、ヒープ割り当てが発生します。
このコミットの文脈では、float64
がinterface{}
に変換される際に、その値がポインタサイズを超えるため、ヒープ上にメモリが割り当てられるという挙動が問題となっていました。
fmt
パッケージとSprintf
fmt
パッケージは、Go言語におけるフォーマットI/Oを提供する標準ライブラリです。Sprintf
関数は、フォーマット文字列と引数を受け取り、フォーマットされた文字列を返します。
func Sprintf(format string, a ...interface{}) string
a ...interface{}
という可変引数は、任意の数の任意の型の引数を受け取れることを意味します。これは、内部的に引数がinterface{}
型のスライスとして扱われるためです。
testing
パッケージとBenchmark
、Test
Go言語のtesting
パッケージは、ユニットテストとベンチマークテストをサポートします。
TestCountMallocs
: このテスト関数は、特定の操作が実行中にどれだけのメモリ割り当て(mallocs)を行ったかを計測します。runtime.MemStats
構造体を使用して、テスト前後のメモリ統計を比較することで、割り当て数を算出します。これは、パフォーマンス最適化やメモリリークの検出に非常に重要なテストです。runtime.MemStats
: Goのランタイムが管理するメモリ統計情報を提供する構造体です。Mallocs
フィールドは、割り当てられたオブジェクトの総数を表します。
Issue 2722
コミットメッセージに「Fixes #2722」とありますが、Web検索では直接このコミットに関連するGoの公式Issueトラッカーのリンクは見つかりませんでした。しかし、コミットメッセージとコードの変更内容から、このIssueはfmt.Sprintf
がfloat64
をinterface{}
として受け取る際のメモリ割り当ての挙動に関するものと推測されます。具体的には、float64
がinterface{}
に変換される際に発生する余分なヒープ割り当てが、テストの期待値と合致しないという問題であったと考えられます。
技術的詳細
このコミットの技術的な核心は、Go言語におけるfloat64
からinterface{}
への変換時のメモリ割り当ての挙動を理解し、それをテストで適切に扱うことにあります。
Goのinterface{}
は、内部的に2つのワード(ポインタサイズ)で構成されます。1つは型情報(_type
ポインタ)、もう1つは値情報(data
ポインタまたは直接値)です。
float32
の場合:float32
は4バイトであり、ポインタサイズ(64bitシステムで8バイト)に収まります。そのため、float32
をinterface{}
に変換しても、その値はインターフェースのdata
フィールドに直接埋め込まれ、ヒープ割り当ては発生しません。float64
の場合:float64
は8バイトであり、64bitシステムではポインタサイズと同じです。しかし、Goのインターフェースの内部実装では、float64
のような値型であっても、その値がインターフェースのdata
フィールドに直接収まらない(あるいは、特定の最適化が適用されない)場合に、ヒープに割り当ててそのポインタをdata
フィールドに格納する挙動を取ることがあります。これが「allocating an extra word for the interface value」という表現の背景にあります。
このコミットでは、Sprintf("%g", 3.14159)
というテストケースにおいて、3.14159
がデフォルトでfloat64
リテラルとして扱われるため、interface{}
への変換時にヒープ割り当てが発生し、期待されるメモリ割り当て数(1)を超えてしまう問題がありました。
この問題を解決するために、テストケースの引数をfloat32(3.14159)
に変更しています。これにより、Sprintf
に渡される値はfloat32
型となり、interface{}
への変換時にヒープ割り当てが発生しなくなります。結果として、Sprintf
関数自体の内部的な割り当て(例えば、結果文字列の生成)のみがカウントされ、テストが期待通りの「1」の割り当て数でパスするようになります。
これは、fmt
パッケージのSprintf
関数自体のメモリ割り当て効率を改善するものではなく、あくまでテストがGoのインターフェースのメモリ割り当て挙動を正確に反映し、意図した通りの計測を行うための修正です。
コアとなるコードの変更箇所
変更はsrc/pkg/fmt/fmt_test.go
ファイルに集中しています。
--- a/src/pkg/fmt/fmt_test.go
+++ b/src/pkg/fmt/fmt_test.go
@@ -509,16 +509,18 @@ func BenchmarkSprintfFloat(b *testing.B) {
var mallocBuf bytes.Buffer
var mallocTest = []struct {
- max int
- desc string
- fn func()
+ count int
+ desc string
+ fn func()
}{
{0, `Sprintf(\"\")`, func() { Sprintf(\"\") }},\n
{1, `Sprintf(\"xxx\")`, func() { Sprintf(\"xxx\") }},\n
{1, `Sprintf(\"%x\")`, func() { Sprintf(\"%x\", 7) }},\n
{2, `Sprintf(\"%s\")`, func() { Sprintf(\"%s\", \"hello\") }},\n
{1, `Sprintf(\"%x %x\")`, func() { Sprintf(\"%x %x\", 7, 112) }},\n
- {2, `Sprintf(\"%g\")`, func() { Sprintf(\"%g\", 3.14159) }}, // TODO: should be 1. See Issue 2722.\n
+ // For %g we use a float32, not float64, to guarantee passing the argument\n
+ // does not need to allocate memory to store the result in a pointer-sized word.\n
+ {2, `Sprintf(\"%g\")`, func() { Sprintf(\"%g\", float32(3.14159)) }},\n
{0, `Fprintf(buf, \"%x %x %x\")`, func() { mallocBuf.Reset(); Fprintf(&mallocBuf, \"%x %x %x\", 7, 8, 9) }},\n
{1, `Fprintf(buf, \"%s\")`, func() { mallocBuf.Reset(); Fprintf(&mallocBuf, \"%s\", \"hello\") }},\n
}\n
@@ -535,8 +537,8 @@ func TestCountMallocs(t *testing.T) {
runtime.UpdateMemStats()\n
mallocs += runtime.MemStats.Mallocs\n
- if mallocs/N > uint64(mt.max) {\n
- t.Errorf(\"%s: expected at most %d mallocs, got %d\", mt.desc, mt.max, mallocs/N)\n
+ if mallocs/N > uint64(mt.count) {\n
+ t.Errorf(\"%s: expected %d mallocs, got %d\", mt.desc, mt.count, mallocs/N)\n
}\n
}\n
}\n
主な変更点は以下の通りです。
mallocTest
構造体のフィールド名がmax
からcount
に変更されました。これは、期待される最大割り当て数ではなく、正確な割り当て数を表すためです。Sprintf("%g", 3.14159)
のテストケースがSprintf("%g", float32(3.14159))
に変更されました。これにより、float64
リテラルではなくfloat32
型が明示的に渡されるようになります。- テストのコメントが追加され、
float32
を使用する理由が説明されています。 TestCountMallocs
関数内のエラーメッセージが、mt.max
からmt.count
を参照するように変更されました。また、「expected at most %d mallocs」から「expected %d mallocs」に変更され、より厳密な期待値の比較を行うようになっています。
コアとなるコードの解説
このコミットの核心は、mallocTest
スライス内の以下の行の変更です。
- {2, `Sprintf("%g")`, func() { Sprintf("%g", 3.14159) }}, // TODO: should be 1. See Issue 2722.
+ // For %g we use a float32, not float64, to guarantee passing the argument
+ // does not need to allocate memory to store the result in a pointer-sized word.
+ {2, `Sprintf("%g")`, func() { Sprintf("%g", float32(3.14159)) }},
元のコードでは、3.14159
という浮動小数点リテラルはGoにおいてデフォルトでfloat64
型として扱われます。このfloat64
型の値がSprintf
の可変引数...interface{}
に渡される際、Goのランタイムはfloat64
の値をinterface{}
に格納するためにヒープ上にメモリを割り当てます。これにより、Sprintf
関数自体の内部的なメモリ割り当てに加えて、このインターフェース変換による割り当てが発生し、合計で2回のメモリ割り当てが計測されていました。しかし、テストの意図としては、Sprintf
関数が文字列をフォーマットする際に発生する割り当てのみをカウントし、その値が1であることを期待していました。
新しいコードでは、float32(3.14159)
と明示的に型変換を行うことで、Sprintf
に渡される値がfloat32
型になります。float32
は4バイトであり、64bitシステムでもポインタサイズ(8バイト)に収まるため、interface{}
への変換時にヒープ割り当てが発生しません。これにより、Sprintf
関数が文字列を生成する際のメモリ割り当てのみがカウントされ、期待通りの1回の割り当てでテストがパスするようになります。
TestCountMallocs
関数内のエラーメッセージの変更も重要です。
- if mallocs/N > uint64(mt.max) {
- t.Errorf("%s: expected at most %d mallocs, got %d", mt.desc, mt.max, mallocs/N)
+ if mallocs/N > uint64(mt.count) {
+ t.Errorf("%s: expected %d mallocs, got %d", mt.desc, mt.count, mallocs/N)
これは、テストの期待値が「最大でmt.max
個」から「正確にmt.count
個」に変わったことを示しています。これにより、テストはより厳密になり、fmt
パッケージのメモリ割り当ての挙動を正確に検証できるようになります。
この修正は、Go言語のインターフェースの内部挙動とメモリ割り当てのメカニズムを深く理解していることを示しており、テストの正確性を高めるための重要な変更です。
関連リンク
- Go言語のインターフェースに関する公式ドキュメントやブログ記事:
- The Laws of Reflection - The Go Programming Language (インターフェースの内部構造について詳しく解説されています)
- Go Data Structures: Interfaces - The Go Programming Language (インターフェースのデータ構造について)
fmt
パッケージのドキュメント: pkg.go.dev/fmttesting
パッケージのドキュメント: pkg.go.dev/testingruntime
パッケージのドキュメント: pkg.go.dev/runtime
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード (
src/pkg/fmt/fmt_test.go
) - Go言語のインターフェースのメモリ割り当てに関する一般的な技術記事や議論 (Web検索結果から得られた一般的な知識)
- コミットメッセージ自体