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

[インデックス 12894] ファイルの概要

このコミットは、Go言語の標準ライブラリであるstrconvパッケージにおけるメモリ確保(malloc)テストの信頼性を向上させることを目的としています。具体的には、既存のftoa_test.goitoa_test.goファイルに分散していたメモリ確保に関するテストロジックをstrconv_test.goという新しい共通のテストファイルに集約し、より堅牢なテストフレームワークを導入することで、テスト結果の安定性を高めています。

コミット

  • コミットハッシュ: 84ef97b59c89b7d9fdc04a1a8a438cd3257bf521
  • 作者: Dave Cheney dave@cheney.net
  • コミット日時: 2012年4月14日 土曜日 21:34:08 +1000
  • コミットメッセージ:
    strconv: make malloc tests more reliable
    
    Fixes #3495.
    
    I adapted fmt.TestCountMallocs to fix the
    existing tests. As the resulting tests did not
    appear to belong to either itoa or ftoa I moved
    them into their own file.
    
    R=bradfitz, fullung
    CC=golang-dev
    https://golang.org/cl/5985072
    

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/84ef97b59c89b7d9fdc04a1a8a438cd3257bf521

元コミット内容

strconv: make malloc tests more reliable

Fixes #3495.

I adapted fmt.TestCountMallocs to fix the
existing tests. As the resulting tests did not
appear to belong to either itoa or ftoa I moved
them into their own file.

R=bradfitz, fullung
CC=golang-dev
https://golang.org/cl/5985072

変更の背景

このコミットの主な背景は、strconvパッケージ内の既存のメモリ確保テストが不安定であったことです。Go言語のコンパイラ最適化(特にエスケープ解析)やランタイムの挙動は、メモリ確保の回数に影響を与える可能性があり、単純なメモリ確保テストでは再現性の問題や誤検出が発生することがありました。

コミットメッセージにあるFixes #3495は、この問題がGoのIssueトラッカーで報告されていたことを示唆しています。作者は、fmtパッケージのTestCountMallocsという既存のテスト手法を参考に、より信頼性の高いメモリ確保テストを実装する必要性を感じていました。

また、ftoa(浮動小数点数から文字列への変換)とitoa(整数から文字列への変換)のテストファイルにそれぞれ独立して存在していたメモリ確保テストが、どちらの機能にも直接的に属さない共通のテストロジックであったため、これらを独立したファイルに集約することで、コードの整理と再利用性を高める意図もありました。

前提知識の解説

Go言語のメモリ管理とアロケーション

Go言語は、自動メモリ管理(ガベージコレクション、GC)を採用しています。開発者はC++のように手動でメモリを解放する必要はありません。Goプログラムが実行される際、新しいオブジェクトが作成されると、そのオブジェクトはヒープ(heap)またはスタック(stack)のいずれかに割り当てられます。

  • スタックアロケーション: 関数内で宣言されたローカル変数など、生存期間が短いオブジェクトは通常スタックに割り当てられます。スタックは高速で、メモリの解放も関数の終了と同時に自動的に行われます。
  • ヒープアロケーション: グローバル変数、関数から返されるオブジェクト、またはコンパイラのエスケープ解析によってヒープに割り当てられると判断されたオブジェクトはヒープに割り当てられます。ヒープに割り当てられたオブジェクトは、ガベージコレクタによって管理され、不要になった時点で解放されます。ヒープアロケーションはスタックアロケーションよりもコストが高く、GCのオーバーヘッドも発生します。

Goのパフォーマンス最適化において、不必要なヒープアロケーションを減らすことは重要な要素の一つです。

テストにおけるメモリ確保の計測

Goのruntimeパッケージは、プログラムの実行時統計情報にアクセスするための機能を提供します。特にruntime.MemStats構造体は、メモリ使用量、ガベージコレクションの統計、そしてメモリ確保(malloc)の回数など、詳細な情報を含んでいます。

  • runtime.ReadMemStats(m *MemStats): 現在のメモリ統計情報をMemStats構造体に読み込みます。
  • m.Mallocs: プログラム開始以降にヒープに割り当てられたオブジェクトの総数を表します。

メモリ確保の回数をテストで計測する一般的なパターンは、テスト対象の処理を実行する前と後でruntime.MemStatsを取得し、Mallocsの差分を計算することです。また、ガベージコレクタの動作がテスト結果に影響を与えないように、テストの前にruntime.GC()を呼び出して強制的にGCを実行することが推奨されます。

エスケープ解析 (Escape Analysis)

エスケープ解析は、Goコンパイラが行う重要な最適化の一つです。これは、変数が関数のスコープを「エスケープ」するかどうか(つまり、関数が終了した後も参照され続ける可能性があるか)を分析し、その結果に基づいて変数をスタックに割り当てるかヒープに割り当てるかを決定します。

例えば、関数内で作成されたオブジェクトがその関数の戻り値として返される場合、そのオブジェクトは関数のスコープをエスケープするため、ヒープに割り当てられる必要があります。しかし、オブジェクトが関数内で完結し、外部に参照が漏れない場合は、スタックに割り当てることが可能です。

エスケープ解析は、不必要なヒープアロケーションを減らし、GCの負荷を軽減することで、プログラムのパフォーマンスを向上させます。しかし、この最適化の挙動はコンパイラのバージョンやコードの書き方によって変化する可能性があり、メモリ確保テストの信頼性を低下させる要因となることがあります。コミットメッセージの// TODO(bradfitz): this might be 0, once escape analysis is betterというコメントは、エスケープ解析の進化によって将来的にアロケーション数が変わる可能性を認識していることを示しています。

strconvパッケージ

strconvパッケージは、Go言語において基本的なデータ型(整数、浮動小数点数、真偽値など)と文字列との間で変換を行うための機能を提供します。例えば、Atoi(文字列から整数へ)、Itoa(整数から文字列へ)、ParseFloat(文字列から浮動小数点数へ)、FormatFloat(浮動小数点数から文字列へ)などの関数があります。

このコミットで言及されているAppendIntAppendFloatは、既存のバイトスライスに数値を文字列として追加する関数で、メモリの再利用を促進し、アロケーションを減らすことを目的としています。

技術的詳細

このコミットの主要な技術的変更点は、メモリ確保テストのロジックを改善し、より信頼性の高い計測を可能にしたことです。

  1. 既存テストの削除:

    • src/pkg/strconv/ftoa_test.goからTestAppendFloatDoesntAllocate関数が削除されました。
    • src/pkg/strconv/itoa_test.goからnumAllocations関数、globalBuf変数、およびTestAppendUintDoesntAllocate関数が削除されました。 これらのテストは、個々のファイルで独立してメモリ確保を計測していましたが、その方法が不安定であったと考えられます。
  2. 新しい共通テストファイルの導入:

    • src/pkg/strconv/strconv_test.goという新しいファイルが作成されました。このファイルは、ftoaitoaといった特定の変換機能に依存しない、strconvパッケージ全体に共通するテストロジックを格納するために使用されます。
  3. TestCountMallocs関数の導入:

    • 新しいstrconv_test.goファイルには、TestCountMallocsという新しいテスト関数が追加されました。この関数は、複数のメモリ確保テストケースをまとめて実行し、その信頼性を高めるための工夫が凝らされています。
  4. mallocTest構造体の導入:

    • TestCountMallocsは、mallocTestという匿名構造体のスライスを利用して、テストケースを定義しています。各テストケースは以下の情報を含みます。
      • count: 期待されるメモリ確保の回数。
      • desc: テストケースの説明。
      • fn: 実際にメモリ確保を発生させる無名関数。
  5. 信頼性向上のための計測ロジック:

    • TestCountMallocs内では、各テストケースのfnN回(ここでは100回)繰り返し実行しています。
    • テスト実行前と実行後にruntime.ReadMemStatsを呼び出し、Mallocsの差分を計算します。
    • runtime.GC()をテストループの前に呼び出すことで、ガベージコレクタの動作がテスト結果に与える影響を最小限に抑えています。
    • mallocs/N > uint64(mt.count)という条件でアサートすることで、単一の実行ではなく、複数回の実行における平均的なメモリ確保回数を検証し、テストの信頼性を向上させています。これにより、一時的なランタイムの挙動やエスケープ解析の揺らぎによる誤検出を減らすことができます。

この変更により、strconvパッケージのメモリ確保に関するテストは、より安定し、Goランタイムやコンパイラの進化にも対応しやすい形になりました。

コアとなるコードの変更箇所

このコミットでは、以下の3つのファイルが変更されています。

  1. src/pkg/strconv/ftoa_test.go

    • TestAppendFloatDoesntAllocate関数が完全に削除されました。
    --- a/src/pkg/strconv/ftoa_test.go
    +++ b/src/pkg/strconv/ftoa_test.go
    @@ -173,23 +173,6 @@ func TestFtoaRandom(t *testing.T) {
     	}\n }\n \n-func TestAppendFloatDoesntAllocate(t *testing.T) {
    -\tn := numAllocations(func() {
    -\t\tvar buf [64]byte
    -\t\tAppendFloat(buf[:0], 1.23, 'g', 5, 64)
    -\t})
    -\twant := 1 // TODO(bradfitz): this might be 0, once escape analysis is better
    -\tif n != want {
    -\t\tt.Errorf("with local buffer, did %d allocations, want %d", n, want)
    -\t}
    -\tn = numAllocations(func() {
    -\t\tAppendFloat(globalBuf[:0], 1.23, 'g', 5, 64)
    -\t})
    -\tif n != 0 {
    -\t\tt.Errorf("with reused buffer, did %d allocations, want 0", n)
    -\t}
    -}\n-\n func BenchmarkFormatFloatDecimal(b *testing.B) {
     	for i := 0; i < b.N; i++ {
     		FormatFloat(33909, 'g', -1, 64)
    
  2. src/pkg/strconv/itoa_test.go

    • runtimeパッケージのインポートが削除されました。
    • numAllocations関数、globalBuf変数、およびTestAppendUintDoesntAllocate関数が完全に削除されました。
    --- a/src/pkg/strconv/itoa_test.go
    +++ b/src/pkg/strconv/itoa_test.go
    @@ -5,7 +5,6 @@
     package strconv_test
     
     import (
    -\t"runtime"
     \t. "strconv"
     \t"testing"
     )
    @@ -126,35 +125,6 @@ func TestUitoa(t *testing.T) {
     	}\n }\n \n-func numAllocations(f func()) int {
    -\truntime.GC()
    -\tmemstats := new(runtime.MemStats)
    -\truntime.ReadMemStats(memstats)
    -\tn0 := memstats.Mallocs
    -\tf()
    -\truntime.ReadMemStats(memstats)
    -\treturn int(memstats.Mallocs - n0)
    -}\n-\n-var globalBuf [64]byte
    -\n-func TestAppendUintDoesntAllocate(t *testing.T) {
    -\tn := numAllocations(func() {
    -\t\tvar buf [64]byte
    -\t\tAppendInt(buf[:0], 123, 10)
    -\t})
    -\twant := 1 // TODO(bradfitz): this might be 0, once escape analysis is better
    -\tif n != want {
    -\t\tt.Errorf("with local buffer, did %d allocations, want %d", n, want)
    -\t}
    -\tn = numAllocations(func() {
    -\t\tAppendInt(globalBuf[:0], 123, 10)
    -\t})
    -\tif n != 0 {
    -\t\tt.Errorf("with reused buffer, did %d allocations, want 0", n)
    -\t}
    -}\n-\n func BenchmarkFormatInt(b *testing.B) {
     	for i := 0; i < b.N; i++ {
     \t\tfor _, test := range itob64tests {
    
  3. src/pkg/strconv/strconv_test.go

    • このファイルは新規作成されました。
    • runtimeパッケージとstrconvパッケージ、testingパッケージがインポートされています。
    • globalBufというグローバルなバイト配列が宣言されています。
    • mallocTestという匿名構造体のスライスが定義され、複数のテストケースが含まれています。
    • TestCountMallocs関数が定義され、mallocTestの各ケースを繰り返し実行し、メモリ確保数を検証するロジックが実装されています。
    --- /dev/null
    +++ b/src/pkg/strconv/strconv_test.go
    @@ -0,0 +1,51 @@
    +// Copyright 2012 The Go Authors. All rights reserved.
    +// Use of this source code is governed by a BSD-style
    +// license that can be found in the LICENSE file.
    +
    +package strconv_test
    +
    +import (
    +	"runtime"
    +	. "strconv"
    +	"testing"
    +)
    +
    +var (
    +	globalBuf [64]byte
    +
    +	mallocTest = []struct {
    +		count int
    +		desc  string
    +		fn    func()
    +	}{
    +		// TODO(bradfitz): this might be 0, once escape analysis is better
    +		{1, `AppendInt(localBuf[:0], 123, 10)`, func() {
    +			var localBuf [64]byte
    +			AppendInt(localBuf[:0], 123, 10)
    +		}},
    +		{0, `AppendInt(globalBuf[:0], 123, 10)`, func() { AppendInt(globalBuf[:0], 123, 10) }},
    +		// TODO(bradfitz): this might be 0, once escape analysis is better
    +		{1, `AppendFloat(localBuf[:0], 1.23, 'g', 5, 64)`, func() {
    +			var localBuf [64]byte
    +			AppendFloat(localBuf[:0], 1.23, 'g', 5, 64)
    +		}},
    +		{0, `AppendFloat(globalBuf[:0], 1.23, 'g', 5, 64)`, func() { AppendFloat(globalBuf[:0], 1.23, 'g', 5, 64) }},
    +	}
    +)
    +
    +func TestCountMallocs(t *testing.T) {
    +	for _, mt := range mallocTest {
    +		const N = 100
    +		memstats := new(runtime.MemStats)
    +		runtime.ReadMemStats(memstats)
    +		mallocs := 0 - memstats.Mallocs
    +		for i := 0; i < N; i++ {
    +			mt.fn()
    +		}
    +		runtime.ReadMemStats(memstats)
    +		mallocs += memstats.Mallocs
    +		if mallocs/N > uint64(mt.count) {
    +			t.Errorf("%s: expected %d mallocs, got %d", mt.desc, mt.count, mallocs/N)
    +		}
    +	}
    +}
    

コアとなるコードの解説

新しく追加されたsrc/pkg/strconv/strconv_test.goファイルが、このコミットの核心部分です。

globalBuf変数

var globalBuf [64]byte

これは、AppendIntAppendFloat関数に渡される再利用可能なバッファとして使用されます。グローバル変数として宣言することで、ヒープアロケーションを避けてスタックに割り当てられることを期待し、アロケーションが0になるべきケースのテストに使用されます。

mallocTest構造体スライス

var (
	globalBuf [64]byte

	mallocTest = []struct {
		count int
		desc  string
		fn    func()
	}{
		// TODO(bradfitz): this might be 0, once escape analysis is better
		{1, `AppendInt(localBuf[:0], 123, 10)`, func() {
			var localBuf [64]byte
			AppendInt(localBuf[:0], 123, 10)
		}},
		{0, `AppendInt(globalBuf[:0], 123, 10)`, func() { AppendInt(globalBuf[:0], 123, 10) }},
		// TODO(bradfitz): this might be 0, once escape analysis is better
		{1, `AppendFloat(localBuf[:0], 1.23, 'g', 5, 64)`, func() {
			var localBuf [64]byte
			AppendFloat(localBuf[:0], 1.23, 'g', 5, 64)
		}},
		{0, `AppendFloat(globalBuf[:0], 1.23, 'g', 5, 64)`, func() { AppendFloat(globalBuf[:0], 1.23, 'g', 5, 64) }},
	}
)

mallocTestは、メモリ確保テストの各シナリオを定義する構造体のスライスです。

  • count: このテストケースで期待される平均メモリ確保回数。
  • desc: テストケースの短い説明。
  • fn: 実際にテスト対象の操作を実行する無名関数。

注目すべきは、AppendIntAppendFloatのテストケースがそれぞれ2つずつある点です。

  1. ローカルバッファを使用する場合: var localBuf [64]byteのように関数内で新しいバッファを宣言してAppendIntAppendFloatに渡すケース。この場合、localBuf自体はスタックに割り当てられますが、AppendIntAppendFloatが内部で追加のメモリを必要とする場合(例えば、引数として渡されたスライスが小さすぎて拡張が必要な場合など)、ヒープアロケーションが発生する可能性があります。コメントにある// TODO(bradfitz): this might be 0, once escape analysis is betterは、エスケープ解析の改善によって将来的にこのアロケーションが0になる可能性を示唆しています。現時点では1回のアロケーションが期待されています。
  2. グローバルバッファを再利用する場合: globalBuf[:0]のように、事前に宣言されたグローバルバッファを再利用するケース。この場合、新しいバッファの作成によるアロケーションは発生しないため、期待されるアロケーション数は0です。これは、Goで効率的なコードを書く際の一般的なパターンであり、メモリの再利用によってパフォーマンスを向上させます。

TestCountMallocs関数

func TestCountMallocs(t *testing.T) {
	for _, mt := range mallocTest {
		const N = 100
		memstats := new(runtime.MemStats)
		runtime.ReadMemStats(memstats)
		mallocs := 0 - memstats.Mallocs // 開始時のMallocs数を記録
		for i := 0; i < N; i++ {
			mt.fn() // テスト対象の操作を実行
		}
		runtime.ReadMemStats(memstats)
		mallocs += memstats.Mallocs // 終了時のMallocs数を加算
		if mallocs/N > uint64(mt.count) {
			t.Errorf("%s: expected %d mallocs, got %d", mt.desc, mt.count, mallocs/N)
		}
	}
}

この関数は、各mallocTestケースをループで処理します。

  • const N = 100: 各テストケースを100回繰り返すことで、計測の信頼性を高めています。単一の実行では、GCのタイミングやスケジューリングによってアロケーション数が変動する可能性があるため、複数回の平均を取ることでより安定した結果を得られます。
  • runtime.ReadMemStats(memstats): テスト対象の操作を実行する前と後でメモリ統計情報を取得します。
  • mallocs := 0 - memstats.Mallocs: テスト開始時点での総アロケーション数を記録します。
  • mallocs += memstats.Mallocs: テスト終了時点での総アロケーション数を加算することで、ループ内で発生したアロケーションの合計数を計算します。
  • if mallocs/N > uint64(mt.count): 100回実行した合計アロケーション数をNで割ることで、1回あたりの平均アロケーション数を算出します。この平均値が期待されるmt.countを超えていないかを検証します。>ではなく>=ではないのは、エスケープ解析の改善などでアロケーションが減る可能性を許容するためと考えられます。

このTestCountMallocsの導入により、strconvパッケージのメモリ確保に関するテストは、より堅牢で信頼性の高いものとなりました。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のメモリ管理とガベージコレクションに関する一般的な情報源(ブログ記事、技術解説など)
  • Go言語のエスケープ解析に関する一般的な情報源(ブログ記事、技術解説など)
  • Go言語のテストに関する一般的な情報源