Goのベンチマークテストを試した
BenchmarkTestの読み方
Goではモジュールの性能試験を行う仕組みが最初から用意されていて、testing.Bを使えば良いらしい。
[Goのベンチマークテストのルール]
- テストの名前はBenchmarkチョメチョメとする ("Benchmark"で名前を始める)
- 引数は *testing.B 、つまりBのポインタ型を受け付ける
- 実行するときは go test に "-bench" オプションをつける
決まりを守ってプログラムを書くと以下のような出力が得られる。
PASS BenchmarkTest1 5000 1573757 ns/op BenchmarkTest2 500 2457098 ns/op ok _/home/echizen/tokyo/app/bench 11.021s
実際に試した
まず、整数のスライスを2つ受け取って、第2引数のスライスに出現する整数が第1引数の中にいくつ含まれるか数える関数を作る。
実装はふたつ用意することとし、一方をスライスを前から順に調べていく方法で、他方をmapを使う方法で作ったものが以下である。
[main.go]
package bench type Pair [2]int // itemsの中からtargetに該当するものを数える func CountUp1(items []int, targets ...int) []Pair { result := make([]Pair, 0, len(targets)) var acc int for _, x := range targets { acc = 0 for _, y := range items { if x == y { acc++ } } result = append(result, Pair{x, acc}) } return result } func CountUp2(items []int, targets ...int) []Pair { xs := make(map[int]int, len(items)) for _, x := range items { xs[x] += 1 } result := make([]Pair, 0, len(targets)) for _, y := range targets { result = append(result, Pair{y, xs[y]}) } return result }
次に、テストの条件(itemsの個数, targetsの個数)を3種類に分けることにした。
- targetsの個数が少ない (50000, 50)
- items, targetsともに多い (50000, 50000)
- itemsの個数が少ない (50, 50000)
テストコードは以下。
[main_test.go]
package bench import ( "math/rand" "testing" ) func makeParams(P int, Q int) (x, y []int) { var items []int var targets []int items = make([]int, P) for i := 0; i < len(items); i++ { items[i] = rand.Intn(Q) } targets = make([]int, Q) for j := 0; j < len(targets); j++ { targets[j] = j } return items, targets } const ITEMS1 = 50000 const TARGETS1 = 50 const ITEMS2 = 50000 const TARGETS2 = 50000 const ITEMS3 = 50 const TARGETS3 = 50000 func BenchmarkTest1Using1(b *testing.B) { items, targets := makeParams(ITEMS1, TARGETS1) b.ResetTimer() for i := 0; i < b.N; i++ { CountUp1(items, targets...) } } func BenchmarkTest1Using2(b *testing.B) { items, targets := makeParams(ITEMS1, TARGETS1) b.ResetTimer() for i := 0; i < b.N; i++ { CountUp2(items, targets...) } } func BenchmarkTest2Using1(b *testing.B) { items, targets := makeParams(ITEMS2, TARGETS2) b.ResetTimer() for i := 0; i < b.N; i++ { CountUp1(items, targets...) } } func BenchmarkTest2Using2(b *testing.B) { items, targets := makeParams(ITEMS2, TARGETS2) b.ResetTimer() for i := 0; i < b.N; i++ { CountUp2(items, targets...) } } func BenchmarkTest3Using1(b *testing.B) { items, targets := makeParams(ITEMS3, TARGETS3) b.ResetTimer() for i := 0; i < b.N; i++ { CountUp1(items, targets...) } } func BenchmarkTest3Using2(b *testing.B) { items, targets := makeParams(ITEMS3, TARGETS3) b.ResetTimer() for i := 0; i < b.N; i++ { CountUp2(items, targets...) } }
実行結果は
(_go)echizen@Seigaku:~/tokyo/app/bench$ go test -bench . testing: warning: no tests to run PASS BenchmarkTest1Using1 500 3207881 ns/op BenchmarkTest1Using2 500 2507589 ns/op BenchmarkTest2Using1 1 2415230671 ns/op BenchmarkTest2Using2 100 11777956 ns/op BenchmarkTest3Using1 500 3034820 ns/op BenchmarkTest3Using2 1000 1892243 ns/op ok _/home/echizen/tokyo/app/bench 15.183s
上記から、このような判断ができる。
- 類型1(50000, 50)だと実装1, 2は互いに有意な結果が得られるまで500回かかったが、実装2の方が3割くらい速い。
- 類型2(50000, 50000)だと、実装1はたった1回やれば十分と判断され、実装2に比べて240倍くらい遅い。
- 類型3(50, 50000)だと、実装2は実装1の2倍の1000回で測定打ち切りになり、実装1より4割くらい速い。
Pythonでやる場合は?
組み込みモジュールにtimeitというやつがあるので、これを使うのがお手軽なんだと思う*1。
[bench.py]
# -*- coding:utf-8 -*- import random def countup1(items, targets): L = [] for target in targets: count = 0 for x in items: if x == target: count += 1 L.append((target, count)) return L def countup2(items, targets): L = [] tmp = {x: 0 for x in targets} for x in items: tmp[x] += 1 return [(x, tmp[x]) for x in targets] def make_params(x, y): return ( [random.randint(1, y) for i in range(x)], list(range(1, y + 1)), ) P1A, P1B = make_params(50000, 50) P2A, P2B = make_params(50000, 50000) P3A, P3B = make_params(50, 50000) if __name__ == '__main__': import timeit items = ( ('P1A', 'P1B', (500, 500)), ('P2A', 'P2B', (1, 100)), ('P3A', 'P3B', (500, 1000)), ) G = globals() for x, y, ns in items: one, two = ns result1 = timeit.timeit( 'countup1({}, {})'.format(x, y), setup='from __main__ import countup1, P1A, P1B, P2A, P2B, P3A, P3B', number=one, ) print('1({}, {})>\t{}\t{}s'.format(len(G[x]), len(G[y]), one, result1)) result2 = timeit.timeit( 'countup2({}, {})'.format(x, y), setup='from __main__ import countup2, P1A, P1B, P2A, P2B, P3A, P3B', number=two, ) print('2({}, {})>\t{}\t{}s'.format(len(G[x]), len(G[y]), two, result2))
[結果]
(py3k)echizen@Seigaku:~/py3k$python bench.py 1(50000, 50)> 500 46.40978667300078s 2(50000, 50)> 500 2.749026679999588s 1(50000, 50000)> 1 94.42981744899953s 2(50000, 50000)> 100 2.782400592999693s 1(50, 50000)> 500 51.12394202299947s 2(50, 50000)> 1000 13.445461807001266s
Pythonのtimeitは試行回数をnumberという名前の引数に指定して動かす。
何回実行してどの程度の速度の差が出るのか等、自分で判断してテストする必要がある。
こうしてみるとGoのtesting.Bの方が手軽に性能試験を試せて良いものだと思った。
参考リンク
テスト結果に出力される数の読み方については特に以下のふたつの記事が参考になった。
Golangでベンチマークを取ってみた - Qiita
Big Sky :: golang でスライスの先頭に追加する append がなぜ遅いのか
その他
Go言語のベンチマークでパフォーマンス測定 | eureka tech blog
Go でベンチマーク - Block Rockin’ Codes
testing - The Go Programming Language
tools/parse.go at master · golang/tools · GitHub
*1:試したらすごく遅かったんだけど何かミスってますかね・・