[インデックス 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検索は行いませんでした。提供された情報と一般的な知識で十分と判断しました。)