読者です 読者をやめる 読者になる 読者になる

Block Rockin’ Codes

back with another one of those block rockin' codes

Go でベンチマーク

golang bench test

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                  詳細出力