[インデックス 14306] ファイルの概要
このコミットは、Go言語のベンチマーク結果を比較するためのユーティリティツールである misc/benchcmp に、メモリ統計(アロケーション数)を表示する機能を追加するものです。これにより、ベンチマーク実行時に -test.benchmem フラグを使用した場合に、操作あたりのメモリ割り当て数(allocs/op)の変化も比較できるようになります。
コミット
commit d5449eafb95cf6b622ad7f569c88e596f9a710a3
Author: Jeff R. Allen <jra@nella.org>
Date: Sat Nov 3 02:13:51 2012 +0800
misc/benchcmp: show memory statistics, when available
R=minux.ma, dave, extraterrestrial.neighbour, rsc
CC=golang-dev
https://golang.org/cl/6587069
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/d5449eafb95cf6b622ad7f569c88e596f9a710a3
元コミット内容
misc/benchcmp: show memory statistics, when available
このコミットは、misc/benchcmp ツールが利用可能な場合にメモリ統計を表示するように変更します。
変更の背景
Go言語のベンチマークツール go test -bench は、関数の実行速度(ns/op)を測定するだけでなく、-benchmem フラグを付与することで、操作あたりのメモリ割り当て量(B/op)とメモリ割り当て回数(allocs/op)も報告できます。
しかし、このコミットが導入される以前の benchcmp ツールは、主に ns/op の比較に特化しており、go test -benchmem で出力されるメモリ関連の統計情報を適切に解析し、比較する機能がありませんでした。パフォーマンスの最適化において、単に実行速度だけでなく、メモリ使用量やアロケーション回数を削減することも非常に重要です。特にガベージコレクションのオーバーヘッドを減らすためには、アロケーション回数の変化を追跡することが不可欠です。
このため、開発者がコード変更によるメモリパフォーマンスへの影響をより詳細に把握できるように、benchcmp がメモリ統計も比較表示できるよう機能拡張する必要がありました。
前提知識の解説
Go言語のベンチマーク (go test -bench)
Go言語には、標準ライブラリの testing パッケージにベンチマーク機能が組み込まれています。ベンチマーク関数は Benchmark プレフィックスで始まり、*testing.B 型の引数を取ります。
package mypackage
import "testing"
func BenchmarkMyFunction(b *testing.B) {
for i := 0; i < b.N; i++ {
// ベンチマーク対象のコード
}
}
ベンチマークは go test -bench=. コマンドで実行できます。このコマンドは、パッケージ内のすべてのベンチマーク関数を実行し、各操作にかかる平均時間(ns/op)を出力します。
メモリ統計のベンチマーク (-benchmem)
go test コマンドに -benchmem フラグを追加すると、ベンチマーク結果にメモリ割り当てに関する統計情報が追加されます。
例: go test -bench=. -benchmem
出力例:
BenchmarkMyFunction-8 100000000 12.3 ns/op 64 B/op 2 allocs/op
ns/op: 1操作あたりのナノ秒。B/op: 1操作あたりのバイト割り当て量。allocs/op: 1操作あたりのメモリ割り当て回数。
このコミットは、特に allocs/op の比較に焦点を当てています。
benchcmp ツール
benchcmp は、Go言語のベンチマーク結果を比較するためのシンプルなコマンドラインツールです。通常、コード変更前と変更後のベンチマーク結果ファイル(old.txt と new.txt など)を引数として受け取り、それぞれのベンチマークの ns/op を比較し、改善率や悪化率を表示します。
例: benchcmp old.txt new.txt
このツールは、go test の出力形式を解析し、ベンチマーク名に基づいて対応する結果を突き合わせます。
awk コマンドとフィールド処理
benchcmp ツールは、シェルスクリプトとして実装されており、内部で awk コマンドを多用して go test の出力を解析しています。
awk は、テキストファイルを処理するための強力なツールです。デフォルトでは、空白文字で区切られた各行の要素を「フィールド」として扱います。
$0: 行全体$1: 1番目のフィールド$2: 2番目のフィールド- ...
$N: N番目のフィールド
awk では、正規表現を使って特定のフィールドの内容をチェックできます。
$N ~ /pattern/: N番目のフィールドがpatternにマッチする場合。
go test -benchmem の出力では、allocs/op の値が特定のフィールド(例えば $8 や $10)に現れることがあります。これは、ベンチマーク関数が b.SetBytes() を使用しているかどうかによって、出力されるフィールドの数が変動するためです。このコミットでは、このフィールドの変動に対応するために $8 と $10 の両方をチェックするロジックが追加されています。
技術的詳細
このコミットの主要な目的は、benchcmp ツールが go test -benchmem の出力からメモリ割り当て回数(allocs/op)を抽出し、比較表示できるようにすることです。
benchcmp は awk スクリプトを使用して go test の出力を解析します。go test -benchmem の出力行は、以下のような形式を取ります。
BenchmarkName-N Iterations Time ns/op Bytes B/op Allocs allocs/op
ここで、Bytes B/op の部分 (MB/s の場合もある) が存在するかどうかによって、allocs/op の値が異なるフィールド番号に現れる可能性があります。
b.SetBytes()を使用しない場合:allocs/opは$8に現れることが多い。b.SetBytes()を使用する場合:MB/sのフィールドが追加されるため、allocs/opは$10に現れることが多い。
このコミットでは、この変動に対応するため、awk スクリプト内で $8 と $10 の両方をチェックし、allocs/op の値が存在すればそれを取得するロジックが追加されました。
具体的には、newalloc と oldalloc という連想配列(awk の配列)が導入され、ベンチマーク名($1)をキーとして、それぞれのベンチマークの allocs/op の値を格納します。
最終的な出力フェーズでは、これらの newalloc と oldalloc の値を使用して、ns/op や MB/s と同様に、allocs/op の「old」「new」「delta」を計算し、整形された形式で表示します。delta はパーセンテージで表示され、oldalloc[what] == 0 の場合は n/a と表示されるように考慮されています。
コアとなるコードの変更箇所
misc/benchcmp ファイルに対する変更は以下の通りです。
--- a/misc/benchcmp
+++ b/misc/benchcmp
@@ -7,8 +7,13 @@ case "$1" in
-*)
echo 'usage: benchcmp old.txt new.txt' >&2
echo >&2
- echo 'Each input file should be go test -test.run=NONE -test.bench=. > [old,new].txt' >&2
+ echo 'Each input file should be from:' >&2
+ echo ' go test -test.run=NONE -test.bench=. > [old,new].txt' >&2
+ echo >&2
echo 'Benchcmp compares the first and last for each benchmark.' >&2
+ echo >&2
+ echo 'If -test.benchmem=true is added to the "go test" command' >&2
+ echo 'benchcmp will also compare memory allocations.' >&2
exit 2
esac
@@ -27,10 +32,21 @@ $1 ~ /Benchmark/ && $4 == "ns/op" {
new[$1] = $3
if($6 == "MB/s")
newmb[$1] = $5
+\
+\ # allocs/op might be at $8 or $10 depending on if
+\ # SetBytes was used or not.
+\ if($8 == "allocs/op")
+\ newalloc[$1] = $7
+\ if($10 == "allocs/op")
+\ newalloc[$1] = $9
} else {
old[$1] = $3
-\ if($6 = "MB/s")
+\ if($6 == "MB/s")
oldmb[$1] = $5
+\ if($8 == "allocs/op")
+\ oldalloc[$1] = $7
+\ if($10 == "allocs/op")
+\ oldalloc[$1] = $9
}
}
@@ -62,5 +78,21 @@ END {
sprintf("%.2f", newmb[what]),
sprintf("%.2f", newmb[what]/oldmb[what]))
}
+\
+\ # print allocs
+\tanyalloc = 0
+\tfor(i=0; i<n; i++) {
+\t\twhat = name[i]\
+\t\tif(!(what in newalloc))\
+\t\t\tcontinue
+\t\tif(anyalloc++ == 0)\
+\t\t\tprintf("\n%-*s %12s %12s %7s\n", len, "benchmark", "old allocs", "new allocs", "delta")
+\t\tif(oldalloc[what] == 0)\
+\t\t\tdelta="n/a"\
+\t\telse
+\t\t\tdelta=sprintf("%.2f", 100*newalloc[what]/oldalloc[what]-100)
+\t\tprintf("%-*s %12d %12d %6s%%\\n", len, what,\
+\t\t\toldalloc[what], newalloc[what], delta)
+\t}
}\
' "$@"
コアとなるコードの解説
-
ヘルプメッセージの更新:
- echo 'Each input file should be go test -test.run=NONE -test.bench=. > [old,new].txt' >&2 + echo 'Each input file should be from:' >&2 + echo ' go test -test.run=NONE -test.bench=. > [old,new].txt' >&2 + echo >&2 + echo 'If -test.benchmem=true is added to the "go test" command' >&2 + echo 'benchcmp will also compare memory allocations.' >&2benchcmpの使用方法を示すヘルプメッセージが更新され、-test.benchmem=trueをgo testコマンドに追加することでメモリ割り当ての比較も可能になることが明示されました。これはユーザーにとって非常に重要な情報です。 -
メモリ割り当て統計の抽出ロジックの追加:
awkスクリプトのBenchmark行を処理する部分に、allocs/opの値を抽出するロジックが追加されました。# allocs/op might be at $8 or $10 depending on if # SetBytes was used or not. if($8 == "allocs/op") newalloc[$1] = $7 if($10 == "allocs/op") newalloc[$1] = $9同様のロジックが
elseブロック(oldデータ用)にも追加されています。newalloc[$1] = $7およびoldalloc[$1] = $7:$8フィールドが"allocs/op"である場合、その前のフィールド$7がallocs/opの値であると判断し、newallocまたはoldalloc配列に格納します。newalloc[$1] = $9およびoldalloc[$1] = $9:$10フィールドが"allocs/op"である場合、その前のフィールド$9がallocs/opの値であると判断し、同様に格納します。 この二重チェックにより、b.SetBytes()の使用有無による出力フォーマットの差異に対応し、allocs/opの値を確実に取得できるようになります。
-
メモリ割り当て統計の出力ロジックの追加:
awkスクリプトのENDブロック(すべての入力行の処理が完了した後)に、メモリ割り当て統計を出力する新しいセクションが追加されました。# print allocs anyalloc = 0 for(i=0; i<n; i++) { what = name[i] if(!(what in newalloc)) continue if(anyalloc++ == 0) printf("\n%-*s %12s %12s %7s\n", len, "benchmark", "old allocs", "new allocs", "delta") if(oldalloc[what] == 0) delta="n/a" else delta=sprintf("%.2f", 100*newalloc[what]/oldalloc[what]-100) printf("%-*s %12d %12d %6s%%\\n", len, what, oldalloc[what], newalloc[what], delta) }anyalloc = 0: メモリ割り当て統計が一つでも存在するかを追跡するためのフラグ。for(i=0; i<n; i++): 処理されたすべてのベンチマーク名をループします。if(!(what in newalloc)) continue:newallocに対応するベンチマークのデータがない場合はスキップします。if(anyalloc++ == 0) printf(...): 最初のメモリ割り当て統計を出力する際に、ヘッダー行(benchmark,old allocs,new allocs,delta)を一度だけ出力します。if(oldalloc[what] == 0) delta="n/a": 以前のallocs/opが0の場合、デルタは計算できないため"n/a"と表示します。else delta=sprintf("%.2f", 100*newalloc[what]/oldalloc[what]-100): それ以外の場合、新しいallocs/opと古いallocs/opの変化率をパーセンテージで計算し、小数点以下2桁で表示します。printf(...): 整形された形式で、ベンチマーク名、古いallocs/op、新しいallocs/op、そしてデルタ(変化率)を出力します。
これらの変更により、benchcmp は単なる時間ベースのパフォーマンス比較ツールから、メモリ割り当ての効率性も評価できる、より包括的なベンチマーク比較ツールへと進化しました。
関連リンク
- Go CL (Code Review): https://golang.org/cl/6587069
参考にした情報源リンク
- Go言語のベンチマークと
-benchmemフラグに関するドキュメントやチュートリアル- https://pkg.go.dev/testing
- https://go.dev/blog/go1.2-benchmarks (Go 1.2 でのベンチマーク改善に関するブログ記事、
B/opとallocs/opの導入について触れられている可能性)
awkコマンドのフィールド処理と正規表現に関するドキュメントbenchcmpツールの機能と使用方法に関する情報 (現在はbenchstatが推奨されているが、当時のbenchcmpの挙動を理解するために参照)- https://pkg.go.dev/golang.org/x/perf/cmd/benchstat (
benchcmpの後継ツールであるbenchstatのドキュメントだが、benchcmpの機能の文脈で役立つ情報が含まれる) - Web検索結果から得られた
benchcmpの使用例や説明。
- https://pkg.go.dev/golang.org/x/perf/cmd/benchstat (