[インデックス 17890] ファイルの概要
このコミットは、Go言語のパフォーマンスベンチマークフレームワークに新たなベンチマークと、それらを駆動するための汎用的なドライバを追加するものです。具体的には、test/bench/perf
ディレクトリ配下に、シンプルなベンチマークの例 (bench1.go
, bench2.go
) と、ベンチマークの実行、時間計測、反復回数の調整を行うための driver.go
が新規追加されています。これにより、Goのランタイムや標準ライブラリの性能特性をより詳細に、かつ自動的に測定・評価するための基盤が強化されます。
コミット
commit 24be1b2b295a95861047efd9afad401945d34e4d
Author: dvyukov <dvyukov@google.com>
Date: Tue Nov 19 12:55:12 2013 +0400
13+
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/24be1b2b295a95861047efd9afad401945d34e4d
元コミット内容
13+
変更の背景
Go言語の開発において、パフォーマンスは常に重要な要素です。ランタイムの最適化や新しい機能の追加が行われる際、その変更が全体のパフォーマンスにどのような影響を与えるかを正確に把握する必要があります。既存のベンチマークツール(go test -bench
)は特定のパッケージ内の関数をベンチマークするのに適していますが、より低レベルなランタイムの挙動や、特定のシナリオにおける性能を詳細に測定するためには、より柔軟なベンチマーク実行環境が必要となる場合があります。
このコミットは、そのようなニーズに応えるために、汎用的なベンチマークドライバと、それを利用するシンプルなベンチマーク例を導入しています。これにより、開発者は特定の性能特性をターゲットにしたカスタムベンチマークを容易に作成し、その結果を自動的に収集・分析できるようになります。コミットメッセージの「13+」は、おそらくGoの内部的なベンチマークスイートのバージョン管理や、特定の性能目標に関連する識別子であると推測されます。
前提知識の解説
このコミットを理解するためには、以下のGo言語の基本的な概念と、一般的なベンチマークに関する知識が必要です。
- Go言語のパッケージと実行可能ファイル: Goのプログラムは
package main
を持つファイルとmain
関数から実行されます。import
文を使って他のパッケージの機能を利用します。 flag
パッケージ: コマンドライン引数を解析するためのGoの標準パッケージです。flag.Int
,flag.Duration
などを使って、整数や時間間隔などの引数を定義し、flag.Parse()
で解析します。time
パッケージ: 時間の計測や操作を行うためのGoの標準パッケージです。time.Duration
: 時間の長さを表す型です。ミリ秒、秒、分などの単位で表現できます。time.Now()
: 現在時刻をtime.Time
型で返します。time.Since(t)
:t
から現在までの経過時間をtime.Duration
型で返します。time.Sleep(d)
: 指定された期間d
だけ現在のゴルーチンをスリープさせます。
- ベンチマークの概念:
- ベンチマーク (Benchmark): ソフトウェアやシステムの性能を測定・評価するためのテストです。特定の操作を繰り返し実行し、その処理時間やリソース消費量を計測します。
- イテレーション (Iteration): ベンチマークにおいて、測定対象の操作を1回実行することです。性能を正確に測るために、通常は多数のイテレーションを繰り返します。
- スループット (Throughput): 単位時間あたりに処理できる操作の数です。
- レイテンシ (Latency): ある操作が開始されてから完了するまでの時間です。
- RSS (Resident Set Size): プロセスが物理メモリ上に確保しているメモリ量です。ベンチマークではメモリ使用量も重要な指標となります。
fmt
パッケージ: フォーマットされた入出力を行うためのGoの標準パッケージです。fmt.Printf
はC言語のprintf
に似た機能を提供します。os
パッケージ: オペレーティングシステムとのやり取り(環境変数、コマンドライン引数、プロセスの終了など)を行うためのGoの標準パッケージです。os.Exit(code)
は指定された終了コードでプログラムを終了します。
技術的詳細
このコミットで追加された主要なファイルは driver.go
であり、これがベンチマーク実行のロジックを担っています。
driver.go
は、以下の主要な関数とロジックで構成されています。
-
コマンドライン引数の定義:
benchNum
: 各ベンチマークを何回実行するか(デフォルト: 3回)。benchTime
: 1回のベンチマーク実行の目標時間(デフォルト: 10秒)。benchMem
: ベンチマークで目指すおおよそのRSS値(MB単位、デフォルト: 64MB)。 これらはflag
パッケージを使って定義され、プログラム実行時に-benchnum=N
,-benchtime=D
,-benchmem=M
のように指定できます。
-
Result
構造体:N
: ベンチマークのイテレーション数。RunTime
: ベンチマークの実行時間。 ベンチマークの結果を保持するためのシンプルな構造体です。
-
main
関数:flag.Parse()
: コマンドライン引数を解析します。- ベンチマークを
*benchNum
回実行し、最も良い結果(RunTime
が最小)をres
に格納します。 - 最終的に、
GOPERF-METRIC:runtime=%v
という形式で、1イテレーションあたりの平均実行時間(ナノ秒)を標準出力に出力します。この形式は、おそらくGoの内部的なベンチマーク結果解析ツールが認識するためのものです。
-
RunBenchmark()
関数:ChooseN(&res)
をループで呼び出し、適切なイテレーション数N
を決定しながらRunOnce(res.N)
を実行します。ChooseN
がfalse
を返す(適切なN
が見つかったか、最大イテレーション数に達した)までループを続けます。- 最終的に、最適な
Result
を返します。
-
RunOnce(N int64)
関数:- 指定されたイテレーション数
N
でベンチマークを1回実行します。 fmt.Printf("Benchmarking %v iterations\\n", N)
で現在のイテレーション数を表示します。t0 := time.Now()
で開始時刻を記録します。err := Benchmark(N)
: ここで、Benchmark
という名前の関数(bench1.go
やbench2.go
で定義されている)を呼び出します。この関数が実際のベンチマーク対象のコードです。- エラーが発生した場合は、メッセージを出力してプログラムを終了します。
res.RunTime = time.Since(t0)
で実行時間を計測し、Result
構造体に格納して返します。
- 指定されたイテレーション数
-
ChooseN(res *Result) bool
関数:- ベンチマークのイテレーション数
N
を動的に調整するロジックです。 - 初回(
last == 0
)はN=1
から開始します。 res.RunTime >= *benchTime
(目標実行時間に達した)またはlast >= MaxN
(最大イテレーション数に達した)場合、false
を返してイテレーション数の調整を終了します。- それ以外の場合、前回の実行結果から1操作あたりの平均時間
nsPerOp
を計算します。 res.N = int64(*benchTime) / nsPerOp
で、目標実行時間内に収まるように新しいイテレーション数N
を推定します。res.N = max(min(res.N+res.N/2, 100*last), last+1)
: この行は、N
を調整するためのヒューリスティックです。res.N+res.N/2
: 推定されたN
を1.5倍することで、より早く目標時間に到達しようとします。100*last
: 前回のN
の100倍を上限とします。急激なN
の増加を防ぎます。last+1
: 少なくとも前回のN
より1多くなるようにします。min
とmax
を使って、N
が適切な範囲に収まるように調整します。
res.N = roundUp(res.N)
: 計算されたN
を丸めます。true
を返し、さらにイテレーション数を調整してベンチマークを続行することを示します。
- ベンチマークのイテレーション数
-
roundUp(n int64)
関数:- 与えられた数値
n
を、1, 2, 5, 10, 20, 50, 100...といった「きれいな」数値に切り上げるヘルパー関数です。 - これは、ベンチマークのイテレーション数がキリの良い数字になるように調整し、結果の可読性を高める目的があると考えられます。
- 与えられた数値
-
min(a, b int64)
とmax(a, b int64)
関数:- 2つの
int64
値の最小値と最大値を返すシンプルなヘルパー関数です。ChooseN
関数でイテレーション数を調整する際に使用されます。
- 2つの
bench1.go
とbench2.go
は、driver.go
が呼び出すBenchmark
関数の具体的な実装例です。
bench1.go
:time.Sleep(time.Duration(N) * time.Millisecond)
を実行するベンチマークです。これは、N
に比例する時間だけスリープする、意図的に遅いベンチマークの例です。bench2.go
: 何もせずnil
を返すだけのベンチマークです。これは、ベンチマークドライバ自体のオーバーヘッドを測定するための、非常に高速なベンチマークの例です。
これらのファイルが新規追加されたことで、Goのパフォーマンス測定システムは、より柔軟で、特定の性能特性に焦点を当てたベンチマークを容易に実行できるようになりました。
コアとなるコードの変更箇所
このコミットは全て新規ファイルの追加であるため、変更箇所は以下の3つのファイルの内容全体となります。
test/bench/perf/bench1.go
package main
import (
"time"
)
func Benchmark(N int64) error {
// 13+
time.Sleep(time.Duration(N) * time.Millisecond)
return nil
}
test/bench/perf/bench2.go
package main
func Benchmark(N int64) error {
return nil
}
test/bench/perf/driver.go
package main
import (
"flag"
"fmt"
"os"
"time"
)
var (
benchNum = flag.Int("benchnum", 3, "run each benchmark that many times")
benchTime = flag.Duration("benchtime", 10*time.Second, "benchmarking time for a single run")
benchMem = flag.Int("benchmem", 64, "approx RSS value to aim at in benchmarks, in MB")
)
type Result struct {
N int64
RunTime time.Duration
}
func main() {
flag.Parse()
var res Result
for i := 0; i < *benchNum; i++ {
res1 := RunBenchmark()
if res.RunTime == 0 || res.RunTime > res1.RunTime {
res = res1
}
}
fmt.Printf("GOPERF-METRIC:runtime=%v\n", int64(res.RunTime)/res.N)
}
func RunBenchmark() Result {
var res Result
for ChooseN(&res) {
res = RunOnce(res.N)
}
return res
}
func RunOnce(N int64) Result {
fmt.Printf("Benchmarking %v iterations\n", N)
t0 := time.Now()
err := Benchmark(N)
if err != nil {
fmt.Printf("Benchmark function failed: %v\n", err)
os.Exit(1)
}
res := Result{N: N}
res.RunTime = time.Since(t0)
return res
}
func ChooseN(res *Result) bool {
const MaxN = 1e12
last := res.N
if last == 0 {
res.N = 1
return true
} else if res.RunTime >= *benchTime || last >= MaxN {
return false
}
nsPerOp := max(1, int64(res.RunTime)/last)
res.N = int64(*benchTime) / nsPerOp
res.N = max(min(res.N+res.N/2, 100*last), last+1)
res.N = roundUp(res.N)
return true
}
func roundUp(n int64) int64 {
tmp := n
base := int64(1)
for tmp >= 10 {
tmp /= 10
base *= 10
}
switch {
case n <= base:
return base
case n <= (2 * base):
return 2 * base
case n <= (5 * base):
return 5 * base
default:
return 10 * base
}
panic("unreachable") // This line is unreachable due to the default case
return 0
}
func min(a, b int64) int64 {
if a < b {
return a
}
return b
}
func max(a, b int64) int64 {
if a > b {
return a
}
return b
}
コアとなるコードの解説
bench1.go
と bench2.go
の Benchmark
関数
これらのファイルは、driver.go
の RunOnce
関数から呼び出される Benchmark
関数の具体的な実装を提供します。
bench1.go
のBenchmark(N int64) error
は、N
ミリ秒間スリープするという、意図的に時間を消費する操作をシミュレートしています。これは、I/Oバウンドな処理や、特定の時間遅延を伴う操作のベンチマークを想定している可能性があります。bench2.go
のBenchmark(N int64) error
は、単にnil
を返すだけで、実質的に何も処理を行いません。これは、ベンチマークドライバ自体のオーバーヘッド(関数呼び出し、時間計測、ループ処理など)を測定するためのベースラインとして機能します。
これらの Benchmark
関数は、Goの標準ベンチマーク(testing
パッケージ)とは異なり、*testing.B
型の引数を受け取らず、N
というイテレーション数を直接受け取るシンプルなインターフェースを持っています。これにより、driver.go
が柔軟にイテレーション数を制御できるようになっています。
driver.go
の主要ロジック
-
main
関数:flag.Parse()
: コマンドライン引数を解析し、benchNum
,benchTime
,benchMem
の値を設定します。for i := 0; i < *benchNum; i++
: ベンチマークを複数回実行し、最も良い結果(実行時間が最も短いもの)を選びます。これは、一時的なシステム負荷やノイズによる測定誤差を軽減し、より安定した結果を得るための一般的なプラクティスです。fmt.Printf("GOPERF-METRIC:runtime=%v\n", int64(res.RunTime)/res.N)
: 最終的な結果を特定のフォーマットで出力します。GOPERF-METRIC:
というプレフィックスは、Goの内部的なパフォーマンス解析ツールがこの行をパースして、ベンチマーク結果を自動的に収集・分析するために使用されることを示唆しています。int64(res.RunTime)/res.N
は、総実行時間をイテレーション数で割ることで、1イテレーションあたりの平均実行時間(ナノ秒単位)を計算しています。
-
RunBenchmark()
関数:- この関数は、
ChooseN
関数とRunOnce
関数を組み合わせて、最適なイテレーション数を見つけながらベンチマークを実行する主要なループを構成します。 for ChooseN(&res)
:ChooseN
がtrue
を返す限りループを続けます。ChooseN
は、次のベンチマーク実行に最適なイテレーション数N
を計算し、まだ目標時間に達していない場合はtrue
を返します。res = RunOnce(res.N)
:ChooseN
で決定されたN
を使って、実際にベンチマークを1回実行し、その結果をres
に格納します。
- この関数は、
-
RunOnce(N int64)
関数:- この関数は、指定されたイテレーション数
N
で実際のベンチマークを実行し、その実行時間を計測します。 t0 := time.Now()
とtime.Since(t0)
を使って、Benchmark(N)
関数の実行時間を正確に計測します。err := Benchmark(N)
: ここが、bench1.go
やbench2.go
で定義された実際のベンチマークコードが呼び出されるポイントです。
- この関数は、指定されたイテレーション数
-
ChooseN(res *Result)
関数:- この関数は、ベンチマークのイテレーション数
N
を動的に調整する賢いロジックを含んでいます。 - 初期化:
last == 0
の場合、最初のイテレーション数N
を1
に設定します。 - 終了条件:
res.RunTime >= *benchTime
(ベンチマークの総実行時間が目標時間benchTime
に達した)またはlast >= MaxN
(イテレーション数が最大値1e12
に達した)の場合、ベンチマークのイテレーション数調整を終了し、false
を返します。 - イテレーション数調整:
nsPerOp := max(1, int64(res.RunTime)/last)
: 前回の実行結果から、1操作あたりの平均実行時間(ナノ秒)を計算します。max(1, ...)
は、時間が非常に短い場合にゼロ除算を防ぐためのガードです。res.N = int64(*benchTime) / nsPerOp
: 目標実行時間*benchTime
を1操作あたりの平均時間で割ることで、目標時間内に実行できるおおよそのイテレーション数を計算します。res.N = max(min(res.N+res.N/2, 100*last), last+1)
: この行は、計算されたN
をさらに調整するためのヒューリスティックです。res.N+res.N/2
: 計算されたN
を1.5倍することで、目標時間に早く到達しようとします。100*last
: 前回のN
の100倍を上限とすることで、イテレーション数が急激に増えすぎるのを防ぎます。last+1
: 少なくとも前回のN
より1多くなるように保証します。これにより、無限ループに陥るのを防ぎます。min
とmax
を組み合わせて、N
が適切な範囲に収まるように調整します。
res.N = roundUp(res.N)
: 計算されたN
をroundUp
関数でキリの良い数値に丸めます。
- この関数は、ベンチマークのイテレーション数
-
roundUp(n int64)
関数:- この関数は、与えられた数値
n
を、1, 2, 5, 10, 20, 50, 100, ... のような「きれいな」数値に切り上げます。例えば、roundUp(12)
は20
を返し、roundUp(34)
は50
を返します。 - これは、ベンチマークのイテレーション数が人間にとって理解しやすい、視覚的に分かりやすい数値になるようにするためのものです。
- この関数は、与えられた数値
この driver.go
のロジックは、Goの標準ベンチマークツール (go test -bench
) が内部的に行っているイテレーション数調整のメカニズムと非常に似ています。目標実行時間に基づいてイテレーション数を動的に調整することで、短時間で信頼性の高いベンチマーク結果を得ることができます。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/doc/
- Go言語の
testing
パッケージ(ベンチマークに関する記述があります): https://pkg.go.dev/testing - Go言語の
flag
パッケージ: https://pkg.go.dev/flag - Go言語の
time
パッケージ: https://pkg.go.dev/time
参考にした情報源リンク
- Go言語のソースコード(GitHub): https://github.com/golang/go
- Goのベンチマークに関する一般的な情報(
go test -bench
の仕組みなど) - Goのパフォーマンス測定に関する記事やドキュメント(一般的なベンチマークのベストプラクティスなど)
- (具体的なURLはWeb検索で得られた情報に基づいて適宜追加)