[インデックス 10405] ファイルの概要
このコミットは、Go言語のリポジトリに misc/benchcmp
という新しいスクリプトを追加するものです。このスクリプトは、Goのベンチマーク結果を比較するためのユーティリティであり、特に2つのベンチマーク実行結果(go test -bench
の出力)を比較し、パフォーマンスの変化を分かりやすく表示することを目的としています。
コミット
- Author: Russ Cox rsc@golang.org
- Date: Tue Nov 15 12:49:22 2011 -0500
- Commit Message:
misc/benchcmp: benchmark comparison script I've been using this since April and posted it on the mailing list, but it seems worth having in the repository. Not sure about the location. R=golang-dev, r, r CC=golang-dev https://golang.org/cl/5371100
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/3db596113d1e663969f68df2cfe6fc36b566663f
元コミット内容
commit 3db596113d1e663969f68df2cfe6fc36b566663f
Author: Russ Cox <rsc@golang.org>
Date: Tue Nov 15 12:49:22 2011 -0500
misc/benchcmp: benchmark comparison script
I've been using this since April and posted it on the
mailing list, but it seems worth having in the repository.
Not sure about the location.
R=golang-dev, r, r
CC=golang-dev
https://golang.org/cl/5371100
---
misc/benchcmp | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 66 insertions(+)
diff --git a/misc/benchcmp b/misc/benchcmp
new file mode 100755
index 0000000000..110c3429e3
--- /dev/null
+++ b/misc/benchcmp
@@ -0,0 +1,66 @@
+#!/bin/sh
+# Copyright 2011 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.
+
+case "$1" in
+-*)\t
+\techo 'usage: benchcmp old.txt new.txt' >&2
+\techo >&2
+\techo 'Each input file should be gotest -bench output.' >&2
+\techo 'Benchcmp compares the first and last for each benchmark.' >&2
+\texit 2
+esac
+
+awk '
+BEGIN {
+ n = 0
+}
+
+$1 ~ /^Benchmark/ && $4 == "ns/op" {
+ if(old[$1]) {
+ if(!saw[$1]++) {
+ name[n++] = $1
+ if(length($1) > len)
+ len = length($1)
+ }
+ new[$1] = $3
+ if($6 == "MB/s")
+ newmb[$1] = $5
+ } else {
+ old[$1] = $3
+ if($6 = "MB/s")
+ oldmb[$1] = $5
+ }
+}
+
+END {
+ if(n == 0) {
+ print "benchcmp: no repeated benchmarks" >"/dev/stderr"
+ exit 1
+ }
+
+ printf("%-*s %12s %12s %7s\\n", len, "benchmark", "old ns/op", "new ns/op", "delta")
+
+ # print ns/op
+ for(i=0; i<n; i++) {
+ what = name[i]
+ printf("%-*s %12d %12d %6s%%\\n", len, what, old[what], new[what],
+ sprintf("%+.2f", 100*new[what]/old[what]-100))
+ }
+
+ # print mb/s
+ anymb = 0
+ for(i=0; i<n; i++) {
+ what = name[i]
+ if(!(what in newmb))
+ continue
+ if(anymb++ == 0)
+ printf("\n%-*s %12s %12s %7s\\n", len, "benchmark", "old MB/s", "new MB/s", "speedup")
+ printf("%-*s %12s %12s %6sx\\n", len, what,
+ sprintf("%.2f", oldmb[what]),
+ sprintf("%.2f", newmb[what]),
+ sprintf("%.2f", newmb[what]/oldmb[what]))
+ }
+}
+' "$@"
変更の背景
このコミットは、Go言語のコア開発者の一人であるRuss Cox氏が、Goのベンチマーク結果を比較するためのスクリプト benchcmp
をGoリポジトリに追加したものです。コミットメッセージによると、Russ Cox氏は2011年4月からこのスクリプトを個人的に使用しており、メーリングリストにも投稿していました。その有用性から、公式リポジトリに含める価値があると判断され、今回のコミットに至りました。
Go言語の開発において、パフォーマンスの回帰を防ぎ、改善を追跡することは非常に重要です。新しいコードの変更が既存のベンチマークにどのような影響を与えるかを迅速に評価するために、2つのベンチマーク実行結果を比較するツールは不可欠です。benchcmp
は、このようなニーズに応えるために作成されました。
前提知識の解説
Go言語のベンチマーク (go test -bench
)
Go言語には、標準でベンチマーク機能が組み込まれています。testing
パッケージを使用し、BenchmarkXxx
という形式の関数を記述することで、コードのパフォーマンスを測定できます。これらのベンチマークは、go test -bench=.
のように -bench
フラグを付けて go test
コマンドを実行することで実行されます。
ベンチマークの出力は通常、以下のような形式になります。
BenchmarkMyFunction-8 100000000 10.5 ns/op
BenchmarkAnotherFunction-8 50000000 25.0 ns/op 10 MB/s
BenchmarkMyFunction-8
: ベンチマーク名とGOMAXPROCSの値(ここでは8コア)。100000000
: 実行回数。10.5 ns/op
: 1操作あたりの平均実行時間(ナノ秒)。10 MB/s
: (オプション)スループット(メモリベンチマークの場合など)。
benchcmp
は、この go test -bench
の出力を2つ(old.txt
と new.txt
)受け取り、それぞれのベンチマークについて ns/op
や MB/s
の値を比較します。
awk
コマンド
awk
は、テキストファイルを行単位で処理し、パターンマッチングとアクションに基づいてデータを操作するための強力なプログラミング言語です。Unix/Linux環境で広く利用されており、ログファイルの解析やデータ変換によく使われます。
awk
スクリプトは通常、パターン { アクション }
の形式で記述されます。
BEGIN { ... }
: 入力処理が始まる前に一度だけ実行されるアクション。END { ... }
: 入力処理がすべて終わった後に一度だけ実行されるアクション。パターン
: 各行がこのパターンにマッチした場合に、対応するアクションが実行されます。パターンが省略された場合、すべて行に対してアクションが実行されます。アクション
: パターンにマッチした行に対して実行される処理。
awk
では、$1
, $2
, ... のようにフィールド変数を使って行の各要素にアクセスできます。例えば、$1
は行の最初のフィールド(通常はスペースで区切られた単語)を指します。連想配列(ハッシュマップ)もサポートしており、array[key] = value
のように使用できます。
benchcmp
スクリプトは、この awk
を利用して go test -bench
の出力を解析し、比較処理を行っています。
技術的詳細
benchcmp
スクリプトは、シェルスクリプトと awk
スクリプトの組み合わせで構成されています。
-
引数処理: スクリプトの冒頭では、引数が適切に渡されているかを確認します。引数が
-
で始まる場合(不正なオプションと見なされる)、正しい使用法 (usage: benchcmp old.txt new.txt
) を表示して終了します。これは、benchcmp
が2つのファイルパスを引数として期待しているためです。 -
awk
スクリプトの実行: 実際の比較ロジックは、埋め込まれたawk
スクリプトによって処理されます。このawk
スクリプトは、引数として渡された2つのファイル(old.txt
とnew.txt
)を読み込みます。-
データ収集フェーズ (
$1 ~ /^Benchmark/ && $4 == "ns/op"
):awk
は入力ファイルを1行ずつ読み込みます。各行がベンチマーク結果の行であるかどうかを$1 ~ /^Benchmark/ && $4 == "ns/op"
というパターンで判定します。$1 ~ /^Benchmark/
: 行の最初のフィールドが "Benchmark" で始まる。$4 == "ns/op"
: 行の4番目のフィールドが "ns/op" である(これにより、時間ベースのベンチマーク結果を特定)。
このパターンにマッチする行が見つかると、以下の処理が行われます。
old[$1]
: もし現在のベンチマーク名($1
)が既にold
配列に存在する場合、それは2番目のファイル(new.txt
)からのデータであると判断し、new[$1]
に$3
(ns/opの値)を格納します。また、MB/s
の値があればnewmb[$1]
に格納します。else
:old
配列に存在しない場合、それは1番目のファイル(old.txt
)からのデータであると判断し、old[$1]
に$3
を、oldmb[$1]
に$5
(MB/sの値)を格納します。name[n++] = $1
: 各ベンチマーク名をname
配列に順序通りに記録します。これにより、後で結果を出力する際に元の順序を維持できます。len = length($1)
: 最も長いベンチマーク名の長さをlen
に保持し、出力のフォーマットに使用します。
-
結果出力フェーズ (
END
): すべての入力ファイルが処理された後、END
ブロックが実行され、比較結果が出力されます。- エラーチェック:
n == 0
の場合、比較対象のベンチマークが見つからなかったことを示し、エラーメッセージを出力して終了します。 - ヘッダー出力:
printf
を使用して、benchmark
,old ns/op
,new ns/op
,delta
のヘッダー行を出力します。%-*s
は、len
の幅で左寄せの文字列を出力するためのprintf
フォーマット指定子です。 - ns/op の比較:
name
配列に記録された各ベンチマーク名についてループし、old
とnew
のns/op
値を比較します。delta
は100 * new[what] / old[what] - 100
で計算され、パーセンテージでの変化量を示します。sprintf("%+.2f", ...)
で小数点以下2桁までの符号付き浮動小数点数としてフォーマットされます。
- MB/s の比較 (オプション):
MB/s
のデータが存在する場合、同様にヘッダー (old MB/s
,new MB/s
,speedup
) と比較結果を出力します。speedup
はnewmb[what] / oldmb[what]
で計算され、速度向上倍率を示します。sprintf("%.2f", ...)
で小数点以下2桁までの浮動小数点数としてフォーマットされます。
- エラーチェック:
-
このスクリプトは、go test -bench
の出力形式に厳密に依存しており、その形式が変更されると正しく動作しなくなる可能性があります。しかし、Goのベンチマーク出力形式は比較的安定しているため、長期間にわたって有用なツールとして機能しています。
コアとなるコードの変更箇所
このコミットでは、misc/benchcmp
という新しいファイルが追加されています。
--- /dev/null
+++ b/misc/benchcmp
@@ -0,0 +1,66 @@
+#!/bin/sh
+# Copyright 2011 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.
+
+case "$1" in
+-*)\t
+\techo 'usage: benchcmp old.txt new.txt' >&2
+\techo >&2
+\techo 'Each input file should be gotest -bench output.' >&2
+\techo 'Benchcmp compares the first and last for each benchmark.' >&2
+\texit 2
+esac
+
+awk '
+BEGIN {
+ n = 0
+}
+
+$1 ~ /^Benchmark/ && $4 == "ns/op" {
+ if(old[$1]) {
+ if(!saw[$1]++) {
+ name[n++] = $1
+ if(length($1) > len)
+ len = length($1)
+ }
+ new[$1] = $3
+ if($6 == "MB/s")
+ newmb[$1] = $5
+ } else {
+ old[$1] = $3
+ if($6 = "MB/s")
+ oldmb[$1] = $5
+ }
+}
+
+END {
+ if(n == 0) {
+ print "benchcmp: no repeated benchmarks" >"/dev/stderr"
+ exit 1
+ }
+
+ printf("%-*s %12s %12s %7s\\n", len, "benchmark", "old ns/op", "new ns/op", "delta")
+
+ # print ns/op
+ for(i=0; i<n; i++) {
+ what = name[i]
+ printf("%-*s %12d %12d %6s%%\\n", len, what, old[what], new[what],
+ sprintf("%+.2f", 100*new[what]/old[what]-100))
+ }
+
+ # print mb/s
+ anymb = 0
+ for(i=0; i<n; i++) {
+ what = name[i]
+ if(!(what in newmb))
+ continue
+ if(anymb++ == 0)
+ printf("\n%-*s %12s %12s %7s\\n", len, "benchmark", "old MB/s", "new MB/s", "speedup")
+ printf("%-*s %12s %12s %6sx\\n", len, what,
+ sprintf("%.2f", oldmb[what]),
+ sprintf("%.2f", newmb[what]),
+ sprintf("%.2f", newmb[what]/oldmb[what]))
+ }
+}
+' "$@"
コアとなるコードの解説
このスクリプトは、#!/bin/sh
で始まるシェルスクリプトであり、内部で awk
コマンドを実行しています。
-
#!/bin/sh
: この行は、スクリプトが/bin/sh
シェルで実行されることを指定します。 -
著作権表示: Goプロジェクトの標準的な著作権表示が含まれています。
-
引数チェック (
case "$1" in -*)
): スクリプトに渡された最初の引数 ($1
) がハイフン (-
) で始まるかどうかをチェックします。これは、ユーザーが誤ってオプションを渡した場合の基本的なエラーハンドリングです。- もしハイフンで始まる場合、
usage
メッセージ(benchcmp old.txt new.txt
)と、入力ファイルがgo test -bench
の出力であるべきこと、そして各ベンチマークの最初と最後の結果を比較することを示す説明を標準エラー出力 (>&2
) に表示し、終了コード2
でスクリプトを終了します。
- もしハイフンで始まる場合、
-
awk
スクリプト本体:awk '...' "$@"
の部分が、実際のベンチマーク比較ロジックです。"$@"
は、スクリプトに渡されたすべての引数(つまりold.txt
とnew.txt
)をawk
コマンドに渡します。-
BEGIN { n = 0 }
:awk
が入力ファイルの処理を開始する前に一度だけ実行されます。変数n
を0
に初期化します。n
は見つかったユニークなベンチマークの数をカウントするために使用されます。 -
$1 ~ /^Benchmark/ && $4 == "ns/op" { ... }
: このブロックは、各入力行がベンチマーク結果の行である場合に実行されます。$1 ~ /^Benchmark/
: 行の最初のフィールドが正規表現^Benchmark
にマッチするかどうかをチェックします。つまり、"Benchmark" で始まるかどうかです。$4 == "ns/op"
: 行の4番目のフィールドが厳密に "ns/op" であるかどうかをチェックします。これにより、時間ベースのベンチマーク結果に絞り込みます。
この条件が真の場合、以下の処理が行われます。
if(old[$1])
: 現在のベンチマーク名($1
)が既にold
連想配列のキーとして存在するかどうかをチェックします。- 存在する場合、それは2番目のファイル(
new.txt
)からのデータであると判断されます。 if(!saw[$1]++)
:saw
連想配列を使って、そのベンチマーク名が初めてnew
データとして現れたかどうかをチェックします。saw[$1]++
は、saw[$1]
の値をインクリメントし、インクリメント前の値を返します。!
はその値を反転させるので、saw[$1]
が0
(つまり初めて) の場合に真となります。name[n++] = $1
: 初めてnew
データとして現れたベンチマーク名をname
配列に追加し、n
をインクリメントします。name
配列は、ベンチマークの元の順序を保持するために使用されます。if(length($1) > len) len = length($1)
: 現在のベンチマーク名の長さが、これまでの最長名len
よりも長い場合、len
を更新します。これは、後で出力する際のフォーマット(列の幅)を調整するために使われます。
new[$1] = $3
: 現在のベンチマークのns/op
値(3番目のフィールド)をnew
連想配列に格納します。if($6 == "MB/s") newmb[$1] = $5
: もし6番目のフィールドが "MB/s" であれば、5番目のフィールド(MB/sの値)をnewmb
連想配列に格納します。
- 存在する場合、それは2番目のファイル(
else
:old[$1]
が存在しない場合、それは1番目のファイル(old.txt
)からのデータであると判断されます。old[$1] = $3
: 現在のベンチマークのns/op
値をold
連想配列に格納します。if($6 = "MB/s") oldmb[$1] = $5
: もし6番目のフィールドが "MB/s" であれば、5番目のフィールドをoldmb
連想配列に格納します。
-
END { ... }
:awk
がすべての入力ファイルの処理を終えた後に一度だけ実行されます。if(n == 0)
:n
が0
の場合(つまり、有効なベンチマークが一つも見つからなかった場合)、エラーメッセージを標準エラー出力に表示し、終了コード1
で終了します。- ヘッダー出力 (ns/op):
printf("%-*s %12s %12s %7s\\n", len, "benchmark", "old ns/op", "new ns/op", "delta")
%-*s
:len
で指定された幅で文字列を左寄せで出力します。%12s
: 12文字幅で文字列を出力します。%7s
: 7文字幅で文字列を出力します。
- ns/op 結果出力:
for(i=0; i<n; i++) { ... }
ループで、name
配列に格納された各ベンチマーク名について結果を出力します。what = name[i]
: 現在のベンチマーク名を取得します。printf("%-*s %12d %12d %6s%%\\n", len, what, old[what], new[what], sprintf("%+.2f", 100*new[what]/old[what]-100))
old[what]
とnew[what]
は、それぞれ古い値と新しい値のns/op
です。sprintf("%+.2f", 100*new[what]/old[what]-100)
: パフォーマンスの変化率を計算し、小数点以下2桁の符号付きパーセンテージ文字列としてフォーマットします。例えば、+5.00%
や-10.25%
のようになります。
- MB/s ヘッダー出力 (オプション):
anymb
変数を使って、MB/s
のデータが一つでも存在する場合にのみヘッダーを出力します。if(anymb++ == 0)
:anymb
が初めて0
の場合に真となり、ヘッダーを出力します。その後anymb
はインクリメントされます。
- MB/s 結果出力:
for(i=0; i<n; i++) { ... }
ループで、MB/s
のデータが存在するベンチマークについて結果を出力します。if(!(what in newmb)) continue
:newmb
にそのベンチマークのデータがない場合はスキップします。printf("%-*s %12s %12s %6sx\\n", len, what, sprintf("%.2f", oldmb[what]), sprintf("%.2f", newmb[what]), sprintf("%.2f", newmb[what]/oldmb[what]))
oldmb[what]
とnewmb[what]
は、それぞれ古い値と新しい値のMB/s
です。sprintf("%.2f", newmb[what]/oldmb[what])
: スループットの向上倍率を計算し、小数点以下2桁の浮動小数点数としてフォーマットします。例えば、1.50x
のようになります。
-
この awk
スクリプトは、Goのベンチマーク出力の特定のフォーマットを効率的に解析し、比較結果を整形して表示する、簡潔かつ強力な例となっています。
関連リンク
- Go言語のベンチマークに関する公式ドキュメント: https://pkg.go.dev/testing (特に
Benchmark
関数のセクション) awk
コマンドに関する情報:- GNU Awk User's Guide: https://www.gnu.org/software/gawk/manual/gawk.html
- Wikipedia (Awk): https://ja.wikipedia.org/wiki/Awk
参考にした情報源リンク
- コミット情報:
/home/violet/Project/comemo/commit_data/10405.txt
- GitHubコミットページ: https://github.com/golang/go/commit/3db596113d1e663969f68df2cfe6fc36b566663f
- Go言語のベンチマークに関する一般的な知識
awk
コマンドに関する一般的な知識- (Web検索は行いませんでした。提供された情報と一般的な知識で十分と判断しました。)