[インデックス 13935] ファイルの概要
このコミットは、Go言語の標準ライブラリ testing パッケージにおけるベンチマーク機能に、メモリ割り当て統計の計測と表示機能を追加するものです。これにより、開発者はベンチマークの実行時間だけでなく、そのベンチマークがどれだけのメモリを割り当て、どれだけのオブジェクトを生成したかを詳細に把握できるようになります。
コミット
commit 74a1a8ae5fb2472d533cc497aee079e7ef52813b
Author: Eric Roshan-Eisner <eric.d.eisner@gmail.com>
Date: Mon Sep 24 15:03:16 2012 -0400
testing: add memory allocation stats to benchmark
R=rsc, nigeltao, dave, bradfitz, r, rogpeppe
CC=golang-dev
https://golang.org/cl/6497084
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/74a1a8ae5fb2472d533cc497aee079e7ef52813b
元コミット内容
このコミットの元の内容は、Goのtestingパッケージにベンチマーク実行時のメモリ割り当て統計(割り当てられたバイト数とオブジェクト数)を追加するというものです。具体的には、go test -benchmemフラグが導入され、ベンチマーク結果にメモリ使用量が表示されるようになります。
変更の背景
Go言語のベンチマークは、コードのパフォーマンスを時間的な観点から評価するために非常に強力なツールです。しかし、パフォーマンスは単に実行時間だけでなく、メモリの使用効率にも大きく依存します。特に、ガベージコレクション(GC)が頻繁に発生するようなメモリ割り当ての多いコードは、実行時間が短くても全体的なシステムパフォーマンスに悪影響を与える可能性があります。
このコミット以前は、Goのベンチマークは主に実行時間(ns/op)と、b.SetBytes()で設定された場合はスループット(MB/s)のみを報告していました。しかし、メモリ割り当ての状況を把握できなければ、開発者はメモリ効率の悪いコードパスを特定し、最適化することが困難でした。
この機能追加の背景には、以下のようなニーズがあったと考えられます。
- メモリリークや過剰なメモリ割り当ての特定: ベンチマーク中に予期せぬ大量のメモリ割り当てが発生していないかを確認する。
- GC負荷の評価: メモリ割り当てが多いとGCが頻繁に実行され、アプリケーションのレイテンシに影響を与えるため、その負荷を定量的に評価する。
- アルゴリズムのメモリ効率比較: 同じ機能を持つ複数のアルゴリズムやデータ構造について、実行時間だけでなくメモリ効率も比較し、より最適な実装を選択する。
- プロファイリングの補助:
pprofなどのプロファイリングツールと連携し、ベンチマークの段階で大まかなメモリ特性を把握することで、より詳細なプロファイリングの必要性を判断する。
これらの背景から、ベンチマーク結果にメモリ割り当て統計を含めることで、開発者がより包括的なパフォーマンス分析を行えるようにすることが目的でした。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念とtestingパッケージの基本的な知識が必要です。
-
Go言語のベンチマーク:
- Goのベンチマークは、
func BenchmarkXxx(*testing.B)という形式の関数として定義されます。 go test -bench=.コマンドで実行され、各操作にかかる時間(ナノ秒/操作、ns/op)やスループット(MB/s)を測定します。*testing.B型の引数bは、ベンチマークの制御(タイマーの開始/停止、イテレーション数の取得など)に使用されます。b.Nは、ベンチマーク関数が実行されるイテレーション数を示し、testingパッケージが自動的に調整して、指定された時間(デフォルト1秒)ベンチマークが実行されるようにします。b.ResetTimer(): タイマーをリセットし、それまでの時間を計測対象から除外します。セットアップコードの時間を計測に含めないために使用されます。b.StartTimer()/b.StopTimer(): タイマーを手動で開始/停止します。特定のコードブロックのみを計測したい場合に利用します。
- Goのベンチマークは、
-
runtime.MemStats:- Goの
runtimeパッケージが提供する構造体で、Goプログラムのメモリ割り当てに関する詳細な統計情報を含んでいます。 runtime.ReadMemStats(m *MemStats)関数を呼び出すことで、現在のメモリ統計をMemStats構造体に読み込むことができます。- 重要なフィールドには以下のようなものがあります。
Mallocs: 割り当てられたオブジェクトの総数。Frees: 解放されたオブジェクトの総数。TotalAlloc: 割り当てられたバイトの総数(解放されたものも含む)。HeapAlloc: 現在ヒープに割り当てられているバイト数。HeapObjects: 現在ヒープに存在するオブジェクトの数。
MemStatsはプログラム全体の累積統計を提供するため、特定のベンチマーク関数内でのメモリ割り当てを正確に測定するには、ベンチマークの開始時と終了時に統計を読み取り、その差分を計算する必要があります。
- Goの
-
sync.Mutex:- Goの
syncパッケージが提供する相互排他ロック(ミューテックス)です。 Lock()メソッドでロックを取得し、Unlock()メソッドでロックを解放します。- 複数のGoroutineが共有リソースに同時にアクセスするのを防ぎ、データ競合を防ぐために使用されます。
- このコミットでは、
runtime.ReadMemStatsがグローバルな統計を読み取るため、複数のベンチマークが並行して実行されると正確な測定ができません。そのため、ベンチマークの実行を直列化するためにsync.Mutexが使用されています。
- Goの
-
Goのガベージコレクション (GC):
- Goは自動メモリ管理(ガベージコレクション)を採用しています。開発者は明示的にメモリを解放する必要がありません。
- GCは、到達不能になったオブジェクトが占めるメモリを自動的に回収します。
- メモリ割り当てが頻繁に行われると、GCの実行頻度が増加し、プログラムの実行が一時停止(ストップ・ザ・ワールド)する時間が長くなる可能性があります。これは、特に低レイテンシが求められるアプリケーションにおいてパフォーマンス上の問題となり得ます。
技術的詳細
このコミットは、Goのベンチマーク機能にメモリ割り当て統計を追加するために、主に以下の技術的変更を加えています。
-
runtime.MemStatsの利用:- ベンチマークの開始時(
StartTimer()内)と終了時(StopTimer()内)にruntime.ReadMemStats()を呼び出し、その時点でのMallocs(割り当てられたオブジェクト数)とTotalAlloc(割り当てられたバイト数)を記録します。 B構造体にstartAllocs,startBytes,netAllocs,netBytesというフィールドを追加し、これらの値を保持します。StopTimer()が呼び出された際に、開始時と終了時の差分を計算し、netAllocsとnetBytesに加算します。これにより、タイマーがオンになっている期間に発生した純粋なメモリ割り当てを測定します。ResetTimer()では、タイマーのリセットと同時にこれらのメモリ統計もリセットされ、新しい計測期間の準備がされます。
- ベンチマークの開始時(
-
ベンチマークの直列実行:
runtime.ReadMemStats()はGoプロセス全体のメモリ統計を報告するため、複数のベンチマークが並行して実行されると、個々のベンチマークの正確なメモリ使用量を測定することができません。- この問題を解決するため、
src/pkg/testing/benchmark.goにbenchmarkLock sync.Mutexというグローバルミューテックスが導入されました。 - 各ベンチマークの
runNメソッド(実際にベンチマーク関数を実行する内部メソッド)の冒頭でbenchmarkLock.Lock()を呼び出し、終了時にdefer benchmarkLock.Unlock()でロックを解放します。これにより、すべてのベンチマークが順次実行されることが保証され、runtime.MemStatsの読み取りが他のベンチマークの活動に影響されなくなります。
-
新しいコマンドラインフラグ
-test.benchmem:var benchmarkMemory = flag.Bool("test.benchmem", false, "print memory allocations for benchmarks")という新しいフラグが追加されました。- このフラグが
trueに設定されている場合(つまり、go test -bench=. -benchmemのように実行された場合)、ベンチマーク結果の出力にメモリ割り当て統計が含まれるようになります。
-
BenchmarkResult構造体の拡張と新しい出力形式:BenchmarkResult構造体にMemAllocs(総割り当てオブジェクト数)とMemBytes(総割り当てバイト数)のフィールドが追加されました。- これらの新しいフィールドに基づいて、
AllocsPerOp()(1操作あたりの割り当てオブジェクト数)とAllocedBytesPerOp()(1操作あたりの割り当てバイト数)というヘルパーメソッドが追加されました。 MemString()という新しいメソッドが追加され、メモリ統計を整形された文字列として出力する機能を提供します。RunBenchmarks関数内で、-test.benchmemフラグが有効な場合にMemString()の結果を既存のベンチマーク結果文字列に追加して出力するように変更されました。
これらの変更により、Goのベンチマークは実行時間だけでなく、メモリ割り当ての観点からもコードのパフォーマンスを評価できる、より強力なツールとなりました。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は、src/pkg/testing/benchmark.goファイルに集中しています。
-
src/pkg/testing/benchmark.go:- フラグとグローバル変数:
var benchmarkMemory = flag.Bool("test.benchmem", false, "print memory allocations for benchmarks") var benchmarkLock sync.Mutex var memStats runtime.MemStats B構造体のフィールド追加:type B struct { // ... 既存フィールド ... startAllocs uint64 startBytes uint64 netAllocs uint64 netBytes uint64 }StartTimer()の変更:func (b *B) StartTimer() { if !b.timerOn { runtime.ReadMemStats(&memStats) // メモリ統計を読み込み b.startAllocs = memStats.Mallocs // 開始時の割り当てオブジェクト数を記録 b.startBytes = memStats.TotalAlloc // 開始時の割り当てバイト数を記録 b.start = time.Now() b.timerOn = true } }StopTimer()の変更:func (b *B) StopTimer() { if b.timerOn { b.duration += time.Now().Sub(b.start) runtime.ReadMemStats(&memStats) // メモリ統計を読み込み b.netAllocs += memStats.Mallocs - b.startAllocs // 差分を計算し加算 b.netBytes += memStats.TotalAlloc - b.startBytes // 差分を計算し加算 b.timerOn = false } }ResetTimer()の変更:func (b *B) ResetTimer() { if b.timerOn { runtime.ReadMemStats(&memStats) // メモリ統計を読み込み b.startAllocs = memStats.Mallocs // 開始時の割り当てオブジェクト数を記録 b.startBytes = memStats.TotalAlloc // 開始時の割り当てバイト数を記録 b.start = time.Now() } b.duration = 0 b.netAllocs = 0 // メモリ統計もリセット b.netBytes = 0 // メモリ統計もリセット }runN()でのロック:func (b *B) runN(n int) { benchmarkLock.Lock() // ベンチマーク実行前にロックを取得 defer benchmarkLock.Unlock() // ベンチマーク実行後にロックを解放 // ... 既存コード ... }BenchmarkResult構造体のフィールド追加:type BenchmarkResult struct { N int // The number of iterations. T time.Duration // The total time taken. Bytes int64 // Bytes processed in one iteration. MemAllocs uint64 // The total number of memory allocations. MemBytes uint64 // The total number of bytes allocated. }BenchmarkResultの新しいメソッド:func (r BenchmarkResult) AllocsPerOp() int64 { ... } func (r BenchmarkResult) AllocedBytesPerOp() int64 { ... } func (r BenchmarkResult) MemString() string { ... }RunBenchmarksでの出力変更:func RunBenchmarks(matchString func(pat, str string) (bool, error), benchmarks []InternalBenchmark) { // ... 既存コード ... results := r.String() if *benchmarkMemory { // -test.benchmem フラグが有効な場合 results += "\t" + r.MemString() // メモリ統計を追加 } fmt.Println(results) // ... 既存コード ... }
- フラグとグローバル変数:
-
src/pkg/testing/testing.go:- コメントの修正:
この変更は、--- a/src/pkg/testing/testing.go +++ b/src/pkg/testing/testing.go @@ -13,7 +13,7 @@ // Functions of the form // func BenchmarkXxx(*testing.B) // are considered benchmarks, and are executed by the "go test" command when -// the -test.bench flag is provided. +// the -test.bench flag is provided. Benchmarks are run sequentially. // // A sample benchmark function looks like this: // func BenchmarkHello(b *testing.B) {benchmarkLockの導入によりベンチマークが直列に実行されるようになったことを反映しています。
- コメントの修正:
コアとなるコードの解説
このコミットの核心は、testingパッケージがベンチマークの実行中にメモリ割り当てを追跡し、その結果を報告するメカニズムを導入した点にあります。
-
メモリ統計の取得と追跡:
B構造体に追加されたstartAllocs,startBytes,netAllocs,netBytesフィールドが、各ベンチマークインスタンスのメモリ割り当て状態を保持します。StartTimer()が呼び出されると、runtime.ReadMemStats(&memStats)で現在のシステム全体のメモリ統計が取得され、その時点のMallocsとTotalAllocがb.startAllocsとb.startBytesに記録されます。これは、ベンチマークの計測開始時点での「ベースライン」となります。StopTimer()が呼び出されると、再度runtime.ReadMemStats(&memStats)で現在の統計が取得され、memStats.Mallocs - b.startAllocsとmemStats.TotalAlloc - b.startBytesによって、タイマーがオンになっていた期間に発生した純粋なメモリ割り当ての差分が計算されます。この差分はb.netAllocsとb.netBytesに累積加算されます。これにより、StartTimer()とStopTimer()が複数回呼び出される場合でも、正確な合計値が計算されます。ResetTimer()は、タイマーをリセットするだけでなく、b.netAllocsとb.netBytesも0にリセットし、新しい計測サイクルでメモリ統計がゼロから開始されるようにします。
-
ベンチマークの直列化:
runtime.ReadMemStatsはGoプロセス全体のメモリ統計を返すため、複数のベンチマークが同時に実行されると、あるベンチマークのメモリ割り当てが別のベンチマークの統計に混入してしまい、正確な測定ができなくなります。- この問題を解決するために、
sync.MutexであるbenchmarkLockが導入されました。runN関数(各ベンチマークのイテレーションを実行する内部関数)の冒頭でbenchmarkLock.Lock()が呼び出され、ベンチマークの実行が開始される前に排他ロックを取得します。defer benchmarkLock.Unlock()により、runN関数が終了する際に必ずロックが解放されます。 - これにより、Goのベンチマークは、たとえ複数のベンチマーク関数が定義されていても、メモリ統計を正確に測定するために一つずつ順番に実行されるようになります。これは
src/pkg/testing/testing.goのコメント変更にも反映されています。
-
結果の集計と表示:
- ベンチマークが完了すると、
B構造体のnetAllocsとnetBytesの値がBenchmarkResult構造体のMemAllocsとMemBytesに格納されます。 BenchmarkResultに新しく追加されたAllocsPerOp()とAllocedBytesPerOp()メソッドは、それぞれMemAllocs / NとMemBytes / Nを計算し、1操作あたりの平均メモリ割り当て数とバイト数を算出します。MemString()メソッドは、これらの値を整形して「B/op」(1操作あたりのバイト数)と「allocs/op」(1操作あたりの割り当て数)の形式で出力します。- 最終的に、
RunBenchmarks関数内で-test.benchmemフラグがtrueの場合に、このMemString()の結果が通常のベンチマーク結果(ns/opやMB/s)に追加されて標準出力に表示されます。
- ベンチマークが完了すると、
これらの変更により、開発者はgo test -bench=. -benchmemを実行するだけで、コードの実行時間だけでなく、そのメモリ効率も同時に評価できるようになり、より包括的なパフォーマンス最適化が可能になりました。
関連リンク
- Go言語の
testingパッケージのドキュメント: https://pkg.go.dev/testing - Go言語の
runtimeパッケージのドキュメント(特にMemStats): https://pkg.go.dev/runtime#MemStats - Go言語の
syncパッケージのドキュメント(特にMutex): https://pkg.go.dev/sync#Mutex - Go言語のベンチマークに関する公式ブログ記事(このコミット以前のものだが、基本的な概念を理解するのに役立つ): https://go.dev/blog/benchmarking
参考にした情報源リンク
- Go言語の公式ドキュメント(
testing,runtime,syncパッケージ) - Go言語のベンチマークに関する一般的な情報源
runtime.MemStatsの動作に関する技術記事sync.Mutexの利用に関する一般的なプログラミング知識- コミットメッセージと変更されたコード自体の詳細な分析