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

[インデックス 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.txtnew.txt)受け取り、それぞれのベンチマークについて ns/opMB/s の値を比較します。

awk コマンド

awk は、テキストファイルを行単位で処理し、パターンマッチングとアクションに基づいてデータを操作するための強力なプログラミング言語です。Unix/Linux環境で広く利用されており、ログファイルの解析やデータ変換によく使われます。

awk スクリプトは通常、パターン { アクション } の形式で記述されます。

  • BEGIN { ... }: 入力処理が始まる前に一度だけ実行されるアクション。
  • END { ... }: 入力処理がすべて終わった後に一度だけ実行されるアクション。
  • パターン: 各行がこのパターンにマッチした場合に、対応するアクションが実行されます。パターンが省略された場合、すべて行に対してアクションが実行されます。
  • アクション: パターンにマッチした行に対して実行される処理。

awk では、$1, $2, ... のようにフィールド変数を使って行の各要素にアクセスできます。例えば、$1 は行の最初のフィールド(通常はスペースで区切られた単語)を指します。連想配列(ハッシュマップ)もサポートしており、array[key] = value のように使用できます。

benchcmp スクリプトは、この awk を利用して go test -bench の出力を解析し、比較処理を行っています。

技術的詳細

benchcmp スクリプトは、シェルスクリプトと awk スクリプトの組み合わせで構成されています。

  1. 引数処理: スクリプトの冒頭では、引数が適切に渡されているかを確認します。引数が - で始まる場合(不正なオプションと見なされる)、正しい使用法 (usage: benchcmp old.txt new.txt) を表示して終了します。これは、benchcmp が2つのファイルパスを引数として期待しているためです。

  2. awk スクリプトの実行: 実際の比較ロジックは、埋め込まれた awk スクリプトによって処理されます。この awk スクリプトは、引数として渡された2つのファイル(old.txtnew.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 配列に記録された各ベンチマーク名についてループし、oldnewns/op 値を比較します。
        • delta100 * new[what] / old[what] - 100 で計算され、パーセンテージでの変化量を示します。sprintf("%+.2f", ...) で小数点以下2桁までの符号付き浮動小数点数としてフォーマットされます。
      • MB/s の比較 (オプション): MB/s のデータが存在する場合、同様にヘッダー (old MB/s, new MB/s, speedup) と比較結果を出力します。
        • speedupnewmb[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 コマンドを実行しています。

  1. #!/bin/sh: この行は、スクリプトが /bin/sh シェルで実行されることを指定します。

  2. 著作権表示: Goプロジェクトの標準的な著作権表示が含まれています。

  3. 引数チェック (case "$1" in -*)): スクリプトに渡された最初の引数 ($1) がハイフン (-) で始まるかどうかをチェックします。これは、ユーザーが誤ってオプションを渡した場合の基本的なエラーハンドリングです。

    • もしハイフンで始まる場合、usage メッセージ(benchcmp old.txt new.txt)と、入力ファイルが go test -bench の出力であるべきこと、そして各ベンチマークの最初と最後の結果を比較することを示す説明を標準エラー出力 (>&2) に表示し、終了コード 2 でスクリプトを終了します。
  4. awk スクリプト本体: awk '...' "$@" の部分が、実際のベンチマーク比較ロジックです。"$@" は、スクリプトに渡されたすべての引数(つまり old.txtnew.txt)を awk コマンドに渡します。

    • BEGIN { n = 0 }: awk が入力ファイルの処理を開始する前に一度だけ実行されます。変数 n0 に初期化します。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 連想配列に格納します。
      • 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): n0 の場合(つまり、有効なベンチマークが一つも見つからなかった場合)、エラーメッセージを標準エラー出力に表示し、終了コード 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のベンチマーク出力の特定のフォーマットを効率的に解析し、比較結果を整形して表示する、簡潔かつ強力な例となっています。

関連リンク

参考にした情報源リンク

  • コミット情報: /home/violet/Project/comemo/commit_data/10405.txt
  • GitHubコミットページ: https://github.com/golang/go/commit/3db596113d1e663969f68df2cfe6fc36b566663f
  • Go言語のベンチマークに関する一般的な知識
  • awk コマンドに関する一般的な知識
  • (Web検索は行いませんでした。提供された情報と一般的な知識で十分と判断しました。)