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

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

このコミットは、Go言語の標準ライブラリのtestingパッケージにAllocsPerRunという新しい関数を追加し、既存のテストコードにおけるメモリ割り当て計測ロジックをこの新しい関数に置き換えるものです。これにより、テストにおけるメモリ割り当ての計測がより簡潔かつ正確に行えるようになります。

コミット

commit 9bfd3c393716d70038788bac102518b901b0d209
Author: Kyle Lemons <kyle@kylelemons.net>
Date:   Sat Feb 2 22:52:29 2013 -0500

    testing: add AllocsPerRun
    
    This CL also replaces similar loops in other stdlib
    package tests with calls to AllocsPerRun.
    
    Fixes #4461.
    
    R=minux.ma, rsc
    CC=golang-dev
    https://golang.org/cl/7002055

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

https://github.com/golang/go/commit/9bfd3c393716d70038788bac102518b901b0d209

元コミット内容

testing: add AllocsPerRun

この変更は、testingパッケージにAllocsPerRun関数を追加します。 また、この変更は、他の標準ライブラリパッケージのテストにおける同様のループをAllocsPerRunの呼び出しに置き換えます。

Fixes #4461.

変更の背景

Go言語では、プログラムのパフォーマンスを最適化する上で、メモリ割り当て(アロケーション)の回数を減らすことが重要です。特に、頻繁に実行されるコードパスや、大量のデータを処理する部分では、不要なメモリ割り当てがパフォーマンスのボトルネックとなることがあります。

これまで、Goのテストコード内で特定の処理がどれくらいのメモリ割り当てを行うかを計測するには、runtime.MemStats構造体を用いて、処理の前後でメモリ統計情報を取得し、Mallocs(割り当て回数)の差分を計算するという手動のプロセスが必要でした。この方法は冗長であり、複数のテストで同様のロジックを記述する必要がありました。また、正確な計測のためには、runtime.GOMAXPROCS(1)を設定して並列処理を一時的に無効化するなどの考慮も必要でした。

このコミットは、これらの手動でのメモリ割り当て計測ロジックを抽象化し、testingパッケージにAllocsPerRunというヘルパー関数を導入することで、テストコードの記述を簡素化し、より信頼性の高いメモリ割り当て計測を可能にすることを目的としています。これにより、開発者はより簡単にコードのメモリ効率を評価し、最適化を進めることができるようになります。

前提知識の解説

Go言語のメモリ管理とガベージコレクション (GC)

Go言語は、自動メモリ管理(ガベージコレクション)を採用しています。開発者はC++のように手動でメモリを解放する必要はありません。しかし、ガベージコレクタは不要になったメモリを自動的に回収しますが、そのプロセス自体にもコストがかかります。特に、頻繁なメモリ割り当てはGCの頻度を増やし、プログラムの実行を一時停止させる「ストップ・ザ・ワールド」時間を増加させる可能性があります。そのため、パフォーマンスが重要なアプリケーションでは、メモリ割り当ての回数を最小限に抑えることが推奨されます。

runtime.MemStats

runtime.MemStatsは、Goプログラムのメモリ使用状況に関する詳細な統計情報を提供する構造体です。これには、ヒープの使用量、GCの統計、そしてメモリ割り当ての回数などが含まれます。

  • Mallocs: プログラムが開始されてから行われたメモリ割り当ての総回数。

runtime.GOMAXPROCS

runtime.GOMAXPROCSは、Goランタイムが同時に実行できるOSスレッドの最大数を設定します。デフォルトでは、CPUの論理コア数に設定されます。メモリ割り当ての計測のようなパフォーマンスに敏感なテストでは、他のゴルーチンやOSスレッドの影響を排除し、より安定した結果を得るために、GOMAXPROCS1に設定することが一般的です。これにより、テスト対象の関数が単一のスレッドで実行され、外部要因による変動が少なくなります。

ベンチマークテストとメモリ割り当て計測

Goのtestingパッケージは、ベンチマークテストをサポートしています。ベンチマークテストは、関数の実行時間やメモリ使用量などのパフォーマンス特性を計測するために使用されます。メモリ割り当ての計測は、特に「ゼロアロケーション」を目指すような最適化において重要な指標となります。ゼロアロケーションとは、特定の処理がヒープメモリを一切割り当てないことを指し、これによりGCのオーバーヘッドを完全に回避できます。

技術的詳細

このコミットの主要な変更点は、src/pkg/testing/allocs.goAllocsPerRun関数が追加されたことです。この関数は、指定された関数fruns回実行した際の平均メモリ割り当て回数を計測します。

AllocsPerRunの内部ロジックは以下の通りです。

  1. GOMAXPROCSの設定: defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))により、関数の実行中はGOMAXPROCSが一時的に1に設定され、関数終了時に元の値に戻されます。これにより、計測中の並列処理による影響を排除し、安定した計測結果を得ます。
  2. ウォームアップ実行: f()を一度実行します。これは、初回実行時に発生する可能性のある初期化コストや、JITコンパイル(GoにはJITはありませんが、類似の最適化)などの影響を排除し、その後の計測がより安定した状態で行われるようにするためです。
  3. 初期メモリ統計の取得: runtime.ReadMemStats(&memstats)を呼び出し、現在のメモリ統計情報(特にMallocs)を取得します。
  4. 関数fの複数回実行: 指定されたruns回だけf()を実行します。
  5. 最終メモリ統計の取得: 再びruntime.ReadMemStats(&memstats)を呼び出し、最終的なメモリ統計情報を取得します。
  6. 平均割り当て回数の計算: (memstats.Mallocs - initial_mallocs) / runsとして、実行回数あたりの平均メモリ割り当て回数を計算し、float64型で返します。ウォームアップ実行での割り当ては計測に含めません。

このAllocsPerRun関数が導入されたことで、既存のテストコードでは、手動でruntime.MemStatsを読み取り、ループ内で関数を実行し、差分を計算するという冗長な処理が、testing.AllocsPerRun(N, func() { ... })という簡潔な記述に置き換えられました。これにより、テストコードの可読性と保守性が向上し、メモリ割り当て計測の正確性も保証されます。

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

このコミットで追加された主要なファイルは以下の通りです。

  • src/pkg/testing/allocs.go: AllocsPerRun関数の定義

また、以下の既存のテストファイルがAllocsPerRunを使用するように変更されています。

  • src/pkg/encoding/gob/timing_test.go
  • src/pkg/fmt/fmt_test.go
  • src/pkg/net/http/header_test.go
  • src/pkg/net/rpc/server_test.go
  • src/pkg/path/filepath/path_test.go
  • src/pkg/path/path_test.go
  • src/pkg/reflect/all_test.go
  • src/pkg/strconv/strconv_test.go
  • src/pkg/time/time_test.go

これらのファイルでは、runtimeパッケージのインポートが削除され、手動でruntime.MemStatsを操作していた部分がtesting.AllocsPerRunの呼び出しに置き換えられています。

コアとなるコードの解説

src/pkg/testing/allocs.go

// Copyright 2013 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 testing

import (
	"runtime"
)

// AllocsPerRun returns the average number of allocations during calls to f.
//
// To compute the number of allocations, the function will first be run once as
// a warm-up.  The average number of allocations over the specified number of
// runs will then be measured and returned.
//
// AllocsPerRun sets GOMAXPROCS to 1 during its measurement and will restore
// it before returning.
func AllocsPerRun(runs int, f func()) (avg float64) {
	defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))

	// Warm up the function
	f()

	// Measure the starting statistics
	var memstats runtime.MemStats
	runtime.ReadMemStats(&memstats)
	mallocs := 0 - memstats.Mallocs // 符号を反転させて、後で加算することで差分を計算する

	// Run the function the specified number of times
	for i := 0; i < runs; i++ {
		f()
	}

	// Read the final statistics
	runtime.ReadMemStats(&memstats)
	mallocs += memstats.Mallocs // 最終的なMallocsから初期値を引く

	// Average the mallocs over the runs (not counting the warm-up)
	return float64(mallocs) / float64(runs)
}

このコードは、AllocsPerRun関数の実装です。

  • defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))は、関数の実行中にGOMAXPROCSを1に設定し、関数が終了する際に元の値に戻すためのものです。これにより、計測の正確性を高めます。
  • f()の最初の呼び出しはウォームアップです。
  • runtime.ReadMemStatsを使って、関数の実行前後のメモリ割り当て回数を取得し、その差分を計算します。
  • mallocs := 0 - memstats.Mallocsという初期化は、memstats.Mallocsが符号なし整数であるため、オーバーフローを避けるために行われます。最終的なmemstats.Mallocsにこの負の値を加算することで、実質的に差分を計算しています。
  • 最終的に、計測された割り当て回数をrunsで割って平均値を算出します。

既存テストファイルの変更例 (src/pkg/encoding/gob/timing_test.goTestCountEncodeMallocs関数)

変更前:

func TestCountEncodeMallocs(t *testing.T) {
	defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(1))
	var buf bytes.Buffer
	enc := NewEncoder(&buf)
	bench := &Bench{7, 3.2, "now is the time", []byte("for all good men")}
	memstats := new(runtime.MemStats)
	runtime.ReadMemStats(memstats)
	mallocs := 0 - memstats.Mallocs
	const count = 1000
	for i := 0; i < count; i++ {
		err := enc.Encode(bench)
		if err != nil {
			t.Fatal("encode:", err)
		}
	}
	runtime.ReadMemStats(memstats)
	mallocs += memstats.Mallocs
	fmt.Printf("mallocs per encode of type Bench: %d\n", mallocs/count)
}

変更後:

func TestCountEncodeMallocs(t *testing.T) {
	const N = 1000

	var buf bytes.Buffer
	enc := NewEncoder(&buf)
	bench := &Bench{7, 3.2, "now is the time", []byte("for all good men")}

	allocs := testing.AllocsPerRun(N, func() {
		err := enc.Encode(bench)
		if err != nil {
			t.Fatal("encode:", err)
		}
	})
	fmt.Printf("mallocs per encode of type Bench: %v\n", allocs)
}

この変更により、runtimeパッケージのインポートが不要になり、手動でのMemStatsの操作やGOMAXPROCSの設定がAllocsPerRunの呼び出しに集約され、コードが大幅に簡素化されています。

関連リンク

参考にした情報源リンク