Go言語の生みの親 Rob Pike氏はプログラミング5原則で次のように言っているそうです。
「推測するな、計測せよ」
ということで、Go言語のパフォーマンス計測についてまとめていきます。
Goのベンチマークについて
Goには標準のテストスイートとしてベンチマーク機能が提供されています。
https://pkg.go.dev/testing#hdr-Benchmarks
今回はベンチマークの測定方法と、
実際に計測を行い、プログラム変更前後でパフォーマンスが改善しているかを確認する方法を見ていきます。
Testingパッケージ(Benchmark)
TestingパッケージのBenchmark使用方法は次の通りです。
用意するもの
・測定したい関数
・ベンチマーク用の関数
ベンチマーク用の関数は、「Benchmark〇〇〇〇」という規則で命名します。
引数にtestingパッケージのtesting.Bを受け取ります。
Go言語でよく言われるsliceのappendパフォーマンス問題を例にとって
ベンチマークの方法について確認していきます。
環境
Go 1.22.1
import "fmt"
// メモリアロケーションの効率が悪い例
func AppendSlice(n int) []string {
res := []string{}
for i := range n {
res = append(res, fmt.Sprintf("No.%d", i))
}
return res
}
package main
import "testing"
func BenchmarkAppendSlice(b *testing.B) {
b.ResetTimer()
for range b.N {
_ = AppendSlice(10000)
}
}
ベンチマークを実行するには、以下のコマンドで行います。
go test -benchmem -bench .
goos: darwin
goarch: arm64
pkg: go_benchmark
BenchmarkAppendSlice-8 1981 598898 ns/op 823940 B/op 19762 allocs/op
PASS
ok go_benchmark 2.407s
出力の見方は、左から順に
ベンチマーク関数名 試行回数 1回の実行にかかる時間(ns/op) メモリアロケーションサイズ(B/op) メモリアロケーション回数(allocs/op)
(補足)
・コマンドの-benchmemはオプションで、メモリの情報も出力できるようになります。
・ベンチマーク関数の引数(b *testing.B)から取得できるb.Nはベンチマーク結果が安定するまで自動で増加する値です。
・b.ResetTimer()は経過時間やメモリアロケーション回数をリセットするので、処理の事前準備など、測定に含めたく無い箇所の影響を除いたりする場合に使用できます。
それでは、このコードを改変してベンチマークの比較を行ってみましょう。
都度appendをせず、はじめに必要な長さのsliceを作成してみます。
package main
import "fmt"
// メモリアロケーションの効率が悪い例
func AppendSlice(n int) []string {
res := []string{}
for i := range n {
res = append(res, fmt.Sprintf("No.%d", i))
}
return res
}
// あらかじめsliceのメモリを確保
func AllocateSlice(n int) []string {
res := make([]string, n)
for i := range n {
res[i] = fmt.Sprintf("No.%d", i)
}
return res
}
package main
import "testing"
func BenchmarkAppendSlice(b *testing.B) {
b.ResetTimer()
for range b.N {
_ = AppendSlice(10000)
}
}
// 追加
func BenchmarkAllocateSlice(b *testing.B) {
b.ResetTimer()
for range b.N {
_ = AllocateSlice(10000)
}
}
go test -benchmem -bench .
goos: darwin
goarch: arm64
pkg: go_benchmark
BenchmarkAppendSlice-8 1998 603473 ns/op 823940 B/op 19762 allocs/op
BenchmarkAllocateSlice-8 2139 552407 ns/op 321630 B/op 19745 allocs/op
PASS
ok go_benchmark 3.753s
比較すると、特にメモリアロケーションサイズ(B/op)が半分以下になっていることが分かります。
実行速度も変更後の方が少し速くなっているようです。
本来は1回の測定では結果に揺らぎが出るので、複数回実施します。
-countオプションで試行回数を指定できます。
go test -count 5 -benchmem -bench .
goos: darwin
goarch: arm64
pkg: go_benchmark
BenchmarkAppendSlice-8 1996 600284 ns/op 823937 B/op 19762 allocs/op
BenchmarkAppendSlice-8 1954 599648 ns/op 823941 B/op 19762 allocs/op
BenchmarkAppendSlice-8 1968 599515 ns/op 823941 B/op 19762 allocs/op
BenchmarkAppendSlice-8 1954 600373 ns/op 823939 B/op 19762 allocs/op
BenchmarkAppendSlice-8 1956 597987 ns/op 823941 B/op 19762 allocs/op
BenchmarkAllocateSlice-8 2056 551929 ns/op 321630 B/op 19745 allocs/op
BenchmarkAllocateSlice-8 2133 553386 ns/op 321631 B/op 19745 allocs/op
BenchmarkAllocateSlice-8 2138 553214 ns/op 321631 B/op 19745 allocs/op
BenchmarkAllocateSlice-8 2125 552679 ns/op 321632 B/op 19745 allocs/op
BenchmarkAllocateSlice-8 2133 552250 ns/op 321629 B/op 19745 allocs/op
PASS
ok go_benchmark 13.512s
まとめ
ベンチマークを計測して、パフォーマンスの比較を簡単に行うことが出来ました。
計測→プログラムの改善のプロセスも気軽に取り入れていきたいですね。
ちなみに冒頭のRob Pike氏の「推測するな、計測せよ」
の原文はこちらです。
You can’t tell where a program is going to spend its time. Bottlenecks occur in surprising places, so don’t try to second guess and put in a speed hack until you’ve proven that’s where the bottleneck is.
Measure. Don’t tune for speed until you’ve measured, and even then don’t unless one part of the code overwhelms the rest.
実際に計測してボトルネックの証拠を見つけるまでは、推測で”speed hack”をするべきでは無い
計測もせずに”speed tune”をしてはいけない
と言うようなことが書いてありますね。実際に計測をしてみて、この原則の理解が少し深まったと思います。
参考
書籍
- 実用Go言語 . O’Reilly Japan ( 渋川 よしき、辻 大志郎、真野 隼記 著 )
- Go言語プログラミングエッセンス . 技術評論社 ( mattn 著 )
web
- testing package https://pkg.go.dev/testing
- go command https://pkg.go.dev/cmd/go@go1.22.1
- Rob Pike’s 5 Rules of Programming https://users.ece.utexas.edu/~adnan/pike.html