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

[インデックス 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)は、特定のSprintfFprintfの呼び出しが期待されるメモリ割り当て数を超えていないかを検証するものです。以前のテストでは、Sprintf("%g", 3.14159)のようにfloat64を直接渡していたため、この余分なメモリ割り当てが発生し、テストが期待通りの結果(割り当て数1)にならず、2つの割り当てが発生していました。コミットメッセージにある「TODO: should be 1. See Issue 2722.」というコメントが、この問題を示唆しています。

このコミットは、このテストの不正確さを修正し、float64interface{}として渡す際のメモリ割り当ての挙動をより正確に反映させることを目的としています。

前提知識の解説

Go言語のインターフェースとメモリ割り当て

Go言語のインターフェースは、動的な型付けを可能にする強力な機能です。インターフェースの値は、内部的に「型」と「値」のペアとして表現されます。

  • 型 (Type): インターフェースに格納されている具体的な値の型情報(例: int, string, *MyStructなど)。
  • 値 (Value): インターフェースに格納されている具体的な値。

この「値」の格納方法には2つのパターンがあります。

  1. 直接格納 (Direct Storage): 値がポインタサイズ(通常は8バイト)に収まる場合、その値はインターフェースの値のフィールドに直接格納されます。例えば、int, bool, float32などのプリミティブ型や、小さな構造体などがこれに該当します。この場合、ヒープ割り当ては発生しません。
  2. 間接格納 (Indirect Storage): 値がポインタサイズを超える場合、その値はヒープに割り当てられ、インターフェースの値のフィールドにはそのヒープ上の値へのポインタが格納されます。例えば、大きな構造体、配列、スライス、マップ、チャネル、そしてfloat64などがこれに該当します。この場合、ヒープ割り当てが発生します。

このコミットの文脈では、float64interface{}に変換される際に、その値がポインタサイズを超えるため、ヒープ上にメモリが割り当てられるという挙動が問題となっていました。

fmtパッケージとSprintf

fmtパッケージは、Go言語におけるフォーマットI/Oを提供する標準ライブラリです。Sprintf関数は、フォーマット文字列と引数を受け取り、フォーマットされた文字列を返します。

func Sprintf(format string, a ...interface{}) string

a ...interface{}という可変引数は、任意の数の任意の型の引数を受け取れることを意味します。これは、内部的に引数がinterface{}型のスライスとして扱われるためです。

testingパッケージとBenchmarkTest

Go言語のtestingパッケージは、ユニットテストとベンチマークテストをサポートします。

  • TestCountMallocs: このテスト関数は、特定の操作が実行中にどれだけのメモリ割り当て(mallocs)を行ったかを計測します。runtime.MemStats構造体を使用して、テスト前後のメモリ統計を比較することで、割り当て数を算出します。これは、パフォーマンス最適化やメモリリークの検出に非常に重要なテストです。
  • runtime.MemStats: Goのランタイムが管理するメモリ統計情報を提供する構造体です。Mallocsフィールドは、割り当てられたオブジェクトの総数を表します。

Issue 2722

コミットメッセージに「Fixes #2722」とありますが、Web検索では直接このコミットに関連するGoの公式Issueトラッカーのリンクは見つかりませんでした。しかし、コミットメッセージとコードの変更内容から、このIssueはfmt.Sprintffloat64interface{}として受け取る際のメモリ割り当ての挙動に関するものと推測されます。具体的には、float64interface{}に変換される際に発生する余分なヒープ割り当てが、テストの期待値と合致しないという問題であったと考えられます。

技術的詳細

このコミットの技術的な核心は、Go言語におけるfloat64からinterface{}への変換時のメモリ割り当ての挙動を理解し、それをテストで適切に扱うことにあります。

Goのinterface{}は、内部的に2つのワード(ポインタサイズ)で構成されます。1つは型情報(_typeポインタ)、もう1つは値情報(dataポインタまたは直接値)です。

  • float32の場合: float32は4バイトであり、ポインタサイズ(64bitシステムで8バイト)に収まります。そのため、float32interface{}に変換しても、その値はインターフェースの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

主な変更点は以下の通りです。

  1. mallocTest構造体のフィールド名がmaxからcountに変更されました。これは、期待される最大割り当て数ではなく、正確な割り当て数を表すためです。
  2. Sprintf("%g", 3.14159)のテストケースがSprintf("%g", float32(3.14159))に変更されました。これにより、float64リテラルではなくfloat32型が明示的に渡されるようになります。
  3. テストのコメントが追加され、float32を使用する理由が説明されています。
  4. 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言語の公式ドキュメント
  • Go言語のソースコード (src/pkg/fmt/fmt_test.go)
  • Go言語のインターフェースのメモリ割り当てに関する一般的な技術記事や議論 (Web検索結果から得られた一般的な知識)
  • コミットメッセージ自体