Block Rockin’ Codes

back with another one of those block rockin' codes

Go でベンチマーク

Intro

Go にはベンチマークを取る仕組みが標準で備わっています。


テストを書く仕組みも標準で備わっており、
testing モジュールと go test コマンドで行いますが、
ベンチも同じような形で実行することができます。


今回は簡単なベンチ取り方を紹介します。
テストについては、そのうちちゃんと触れます。


参考ソースとして、こちらを使います。

https://github.com/Jxck/swrap

対象のコード

例えば以下のようなコードがあって、そのベンチを取るとします。

// swrap.go

type SWrap []byte

func (sw *SWrap) Add(a byte) {
	*sw = append(*sw, a)
}

func (sw *SWrap) Len() int {
	return len(*sw)
}

Slice にメソッドを生やしただけです。

ベンチマーク関数

ベンチマークは、 *_test.go ファイルに書きます。
関数名は Bench で始め、引数として *testing.B を渡す関数として定義します。
(このへんはテストと同じです。)

// swrap_test.go

import (
	"testing"
)

func BenchmarkAdd(b *testing.B) {
	for i := 0; i < b.N; i++ {
		sw := New(Fixture())
		sw.Add(0xFF)
	}
}

func BenchmarkLen(b *testing.B) {
	sw := New(Fixture())
	b.ResetTimer()

	for i := 0; i < b.N; i++ {
		sw.Len()
	}
}

b.N が勝手に設定されるので、それをループの条件とするだけです。
ループにかかる時間を測るので、ループに入る前の準備処理などに時間がかかる場合は、
ループの前に b.ResetTimer() を呼ぶと準備処理分を引くことができます。

実行方法

実行は go test にオプションを追加するだけです。($GOPATH を通しておいて下さい)

$ go test -bench Add   # Add とつくものだけ実行
$ go test -bench .     # 全て実行

全ての実行例はこんな感じです。(インデントだけ整形済み)

$ go test -bench .
PASS
BenchmarkNew      2000000000   0.64 ns/op
BenchmarkBytes    2000000000   1.09 ns/op
BenchmarkLen      2000000000   1.23 ns/op
BenchmarkAdd      5000000      334 ns/op
BenchmarkMerge    10000000     254 ns/op
BenchmarkDelete   20000000     148 ns/op
BenchmarkCompare  50000000     32.8 ns/op
BenchmarkPush     5000000      354 ns/op
BenchmarkPop      10000000     114 ns/op
BenchmarkShift    10000000     234 ns/op
BenchmarkUnShift  20000000     109 ns/op
BenchmarkReplace  5000000      614 ns/op
ok    swrap 27.847s

実行回数が違うのは、 b.N の値が変わっているからですね。
この b.N は直接指定するのではなく、ベンチの実行時間からの逆算です。
デフォルトは 1 秒間実行しているようです。
この実行時間は -benchtime で指定できます。
十分な時間を指定し、性能自体は ns/op で見るのが良さそうです。

benchmem

"-benchmem" オプションをつけるとメモリアロケート回数がわかります。
変数をポインタに変更するなどの改善ポイントが見つけられます。

PASS
BenchmarkLen	2000000000	0.95 ns/op	0 B/op	0 allocs/op
BenchmarkAdd	 5000000	370 ns/op	48 B/op	2 allocs/op

比較

実はこの結果を比較するツールがひっそりと同梱されています。

まったくいいサンプルが無いんだけど手元にあったもので、
例えば以下の様な超地味なチューニングに成功したとします。

PASS
BenchmarkNew	2000000000	0.47 ns/op	0 B/op	0 allocs/op
ok  	swrap	1.000s
PASS
BenchmarkNew	2000000000	0.44 ns/op	0 B/op	0 allocs/op
ok  	swrap	0.954s

それぞれ before after で保存して
これを misc 以下にある benchcmp に渡します。

$ $GOROOT/misc/benchcmp before after
benchmark       old ns/op    new ns/op    delta
BenchmarkNew            0            0   +6.82%

こうした値をパッチに添えて PR とかすると話が速くていいかもしれませんね。

オプション

オプションを簡単(適当)に訳しておきます。
(まだ使ったことがないものが多いです)

http://golang.org/cmd/go/#hdr-Description_of_testing_flags

-bench regexp       対象の指定
-benchmem           メモリアロケーションの表示
-benchtime          ベンチ実行時間
-blockprofile       goroutine のブロックプロファイルを出力
-blockprofilerate   runtime.SetBlockProfileRate の値を指定
-cpu                GOMAXPROCS の値を指定
-cpuprofile         CPU プロファイルを出力
-memprofile         メモリプロファイルを出力
-memprofilerate     runtime.MemProfileRate の値を指定
-parallel           並列実行の指定(デフォルトは GOMAXPROCS)
-run                テストと example の実行対象指定
-short              時間を短縮して簡易実行
-timeout            タイムアウト
-v                  詳細出力