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

[インデックス 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 は、以下の主要な関数とロジックで構成されています。

  1. コマンドライン引数の定義:

    • benchNum: 各ベンチマークを何回実行するか(デフォルト: 3回)。
    • benchTime: 1回のベンチマーク実行の目標時間(デフォルト: 10秒)。
    • benchMem: ベンチマークで目指すおおよそのRSS値(MB単位、デフォルト: 64MB)。 これらはflagパッケージを使って定義され、プログラム実行時に-benchnum=N, -benchtime=D, -benchmem=Mのように指定できます。
  2. Result構造体:

    • N: ベンチマークのイテレーション数。
    • RunTime: ベンチマークの実行時間。 ベンチマークの結果を保持するためのシンプルな構造体です。
  3. main関数:

    • flag.Parse(): コマンドライン引数を解析します。
    • ベンチマークを*benchNum回実行し、最も良い結果(RunTimeが最小)をresに格納します。
    • 最終的に、GOPERF-METRIC:runtime=%vという形式で、1イテレーションあたりの平均実行時間(ナノ秒)を標準出力に出力します。この形式は、おそらくGoの内部的なベンチマーク結果解析ツールが認識するためのものです。
  4. RunBenchmark()関数:

    • ChooseN(&res)をループで呼び出し、適切なイテレーション数Nを決定しながらRunOnce(res.N)を実行します。
    • ChooseNfalseを返す(適切なNが見つかったか、最大イテレーション数に達した)までループを続けます。
    • 最終的に、最適なResultを返します。
  5. RunOnce(N int64)関数:

    • 指定されたイテレーション数Nでベンチマークを1回実行します。
    • fmt.Printf("Benchmarking %v iterations\\n", N)で現在のイテレーション数を表示します。
    • t0 := time.Now()で開始時刻を記録します。
    • err := Benchmark(N): ここで、Benchmarkという名前の関数(bench1.gobench2.goで定義されている)を呼び出します。この関数が実際のベンチマーク対象のコードです。
    • エラーが発生した場合は、メッセージを出力してプログラムを終了します。
    • res.RunTime = time.Since(t0)で実行時間を計測し、Result構造体に格納して返します。
  6. 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多くなるようにします。
      • minmaxを使って、Nが適切な範囲に収まるように調整します。
    • res.N = roundUp(res.N): 計算されたNを丸めます。
    • trueを返し、さらにイテレーション数を調整してベンチマークを続行することを示します。
  7. roundUp(n int64)関数:

    • 与えられた数値nを、1, 2, 5, 10, 20, 50, 100...といった「きれいな」数値に切り上げるヘルパー関数です。
    • これは、ベンチマークのイテレーション数がキリの良い数字になるように調整し、結果の可読性を高める目的があると考えられます。
  8. min(a, b int64)max(a, b int64)関数:

    • 2つのint64値の最小値と最大値を返すシンプルなヘルパー関数です。ChooseN関数でイテレーション数を調整する際に使用されます。

bench1.gobench2.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.gobench2.goBenchmark 関数

これらのファイルは、driver.goRunOnce 関数から呼び出される Benchmark 関数の具体的な実装を提供します。

  • bench1.goBenchmark(N int64) error は、Nミリ秒間スリープするという、意図的に時間を消費する操作をシミュレートしています。これは、I/Oバウンドな処理や、特定の時間遅延を伴う操作のベンチマークを想定している可能性があります。
  • bench2.goBenchmark(N int64) error は、単に nil を返すだけで、実質的に何も処理を行いません。これは、ベンチマークドライバ自体のオーバーヘッド(関数呼び出し、時間計測、ループ処理など)を測定するためのベースラインとして機能します。

これらの Benchmark 関数は、Goの標準ベンチマーク(testingパッケージ)とは異なり、*testing.B 型の引数を受け取らず、N というイテレーション数を直接受け取るシンプルなインターフェースを持っています。これにより、driver.go が柔軟にイテレーション数を制御できるようになっています。

driver.go の主要ロジック

  1. 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イテレーションあたりの平均実行時間(ナノ秒単位)を計算しています。
  2. RunBenchmark() 関数:

    • この関数は、ChooseN 関数と RunOnce 関数を組み合わせて、最適なイテレーション数を見つけながらベンチマークを実行する主要なループを構成します。
    • for ChooseN(&res): ChooseNtrue を返す限りループを続けます。ChooseN は、次のベンチマーク実行に最適なイテレーション数 N を計算し、まだ目標時間に達していない場合は true を返します。
    • res = RunOnce(res.N): ChooseN で決定された N を使って、実際にベンチマークを1回実行し、その結果を res に格納します。
  3. RunOnce(N int64) 関数:

    • この関数は、指定されたイテレーション数 N で実際のベンチマークを実行し、その実行時間を計測します。
    • t0 := time.Now()time.Since(t0) を使って、Benchmark(N) 関数の実行時間を正確に計測します。
    • err := Benchmark(N): ここが、bench1.gobench2.go で定義された実際のベンチマークコードが呼び出されるポイントです。
  4. ChooseN(res *Result) 関数:

    • この関数は、ベンチマークのイテレーション数 N を動的に調整する賢いロジックを含んでいます。
    • 初期化: last == 0 の場合、最初のイテレーション数 N1 に設定します。
    • 終了条件: 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多くなるように保証します。これにより、無限ループに陥るのを防ぎます。
        • minmax を組み合わせて、N が適切な範囲に収まるように調整します。
      • res.N = roundUp(res.N): 計算された NroundUp 関数でキリの良い数値に丸めます。
  5. roundUp(n int64) 関数:

    • この関数は、与えられた数値 n を、1, 2, 5, 10, 20, 50, 100, ... のような「きれいな」数値に切り上げます。例えば、roundUp(12)20 を返し、roundUp(34)50 を返します。
    • これは、ベンチマークのイテレーション数が人間にとって理解しやすい、視覚的に分かりやすい数値になるようにするためのものです。

この driver.go のロジックは、Goの標準ベンチマークツール (go test -bench) が内部的に行っているイテレーション数調整のメカニズムと非常に似ています。目標実行時間に基づいてイテレーション数を動的に調整することで、短時間で信頼性の高いベンチマーク結果を得ることができます。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード(GitHub): https://github.com/golang/go
  • Goのベンチマークに関する一般的な情報(go test -bench の仕組みなど)
  • Goのパフォーマンス測定に関する記事やドキュメント(一般的なベンチマークのベストプラクティスなど)
    • (具体的なURLはWeb検索で得られた情報に基づいて適宜追加)