【Go】Goのテストに入門してみた! ~ベンチマーク編~
はじめに
前回は「カバレッジ」を見ていきました。
今回は「ベンチマーク」に入門していきます!
前回の記事はこちら!
この記事でわかること
- ベンチマークとは何か
- Goのベンチマークテストの書き方
- ベンチマークテスト結果の見方
ベンチマークとは?
ベンチマークとは、プログラムの処理性能(速度やメモリ使用量)を数値的に測定する方法です。
Goでは、testingパッケージ を使って、関数の実行時間やメモリ消費量、アロケーションの頻度を自動で計測できます。
ベンチマークを使うことで、
- 「どの実装がより速いのか?」
- 「どれだけメモリを使っているのか?」
- 「処理効率は改善されているか?」
といった実用的な指標が得られ、パフォーマンスチューニングの根拠になります。
ベンチマークをとるための実装
今回は、以前の記事でもやった「文字列結合」を例にベンチマークを取ります。
package main
import (
"fmt"
"strings"
"testing"
)
// 10万個の文字列を作成する
const N = 100000
// "Hello"という文字列が指定した分だけ配列に格納される
func generateStrings() []string {
s := make([]string, N)
b.ResetTimer()
for i := 0; i < N; i++ {
s[i] = "Hello"
}
return s
}
// +演算子を用いた文字列結合
func BenchmarkStringConcatPlus(b *testing.B) {
s := generateStrings()
b.ResetTimer()
for i := 0; i < b.N; i++ {
result := ""
for _, v := range s {
result += v + " "
}
_ = result
}
}
// fmt.Sprintfを用いた文字列結合
func BenchmarkStringConcatSprintf(b *testing.B) {
s := generateStrings()
b.ResetTimer()
for i := 0; i < b.N; i++ {
result := ""
for _, v := range s {
result = fmt.Sprintf("%s %s", result, v)
}
_ = result
}
}
// stringsパッケージのJoin関数を用いた文字列結合
func BenchmarkStringConcatJoin(b *testing.B) {
s := generateStrings()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = strings.Join(s, " ")
}
}
// stringsパッケージのBuilderを用いた文字列結合
func BenchmarkStringConcatBuilder(b *testing.B) {
s := generateStrings()
b.ResetTimer()
for i := 0; i < b.N; i++ {
var sb strings.Builder
for _, v := range s {
sb.WriteString(v)
sb.WriteString(" ")
}
_ = sb.String()
}
}
1. 関数名をBenchmarkからはじめる
ベンチマークをとるときは、必ず関数名を Bechmark からはじめるようにします。
2. 引数にはb *testing.Bを渡す
ベンチマーク関数の引数には、b *testing.B を渡します。
3. ループ回数にはb.Nを使う
ベンチマーク関数のループ回数には、b.N を 使います。
Goのベンチマークは、一定の精度を保つために、処理時間に応じて b.N を動的に調整します。
- 処理が早い場合 → 試行回数を多めにして安定した計測を行う
- 処理が遅い場合 → 少ない回数でも十分なデータが取れるので、試行回数を少なくする
4. b.ResetTimer()を使う
ベンチマーク関数の中で初期化処理を行っていた場合、それはベンチマークの対象外なので、b.ResetTimer() を使ってリセットを行います。
ベンチマークをとってみよう
以下のコマンドでベンチマークをとることができます。
# go test -benchmem -bench .
## -benchmem:ベンチマーク時のメモリ割り当て情報を出力
## -bench . :実行するベンチマーク関数を指定(.は全指定を表す)
goos: darwin
goarch: arm64
pkg: go-test-practice
cpu: Apple M1
BenchmarkStringConcatPlus-8 1 4679751291 ns/op 30394878864 B/op 100174 allocs/op
BenchmarkStringConcatSprintf-8 1 4964243791 ns/op 60439051232 B/op 397670 allocs/op
BenchmarkStringConcatJoin-8 1533 755007 ns/op 607255 B/op 1 allocs/op
BenchmarkStringConcatBuilder-8 1398 856217 ns/op 3243900 B/op 31 allocs/op
PASS
ok go-test-practice 13.496s
1. goos
使用しているOSを指します。
この例では、Darwin(Appleが開発するOSで、macOSの基礎部分)が該当します。
2. goarch
使用しているアーキテクチャを指します。
この例では、Apple Siliconのarm64が該当します。
3. pkg
テストを行ったパッケージ名を指します。
ここではgo-test-practiceが該当します。
4. cpu
使用しているCPU情報を指します。
この例では Apple M1が該当します。
5. ベンチマーク結果
| 項目 | 意味 |
|---|---|
BenchmarkStringConcatPlus-8 |
ベンチマーク関数の名前。末尾の-8は 並列性(GOMAXPROCSの値) を表します。この例では8スレッド並列で計測されたことを示しています。 |
1 |
実行されたベンチマークの回数(b.N の値)。ここでは1回しか実行されなかったため、非常に重い処理であることがわかります。 |
4679751291 ns/op |
1回の実行にかかった平均時間(ナノ秒)。この例では約4.67 秒です。 |
30394878864 B/op |
1回の操作あたりのメモリ使用量(バイト単位)。この例では 約30GB のメモリが使われたことを示しており、かなり非効率な処理と言えます。 |
100174 allocs/op |
1回の操作あたりに発生したアロケーション回数。ここでは 10万回以上のアロケーションが発生しています。 |
つまり、以下の結果が効率的な処理をしていると言うことができます。
-
1回の実行にかかった平均時間が短い
- 高速で処理が完了しているので、CPU効率が良い
-
1回の操作あたりのメモリ使用量が小さい
- メモリ使用量が少なく、キャッシュに収まりやすくなり、GCの負荷も減る
-
アロケーション回数が少ない
- メモリ確保の頻度が少ないのでGCが発生しにくくなり、処理が安定する
まとめ
今回は「ベンチマーク」について学びました。
ベンチマークとり、それぞれの処理時間・メモリ使用量・アロケーション数を比較することで、効率的な実装が何かを数値で判断できます。
パフォーマンスが重要な処理において、ベンチマークは「なんとなく速そう」ではなく、「実際に速い」かどうかを証明する有効な手段です。
次回は「テーブル駆動ベンチマークテスト」に入門します!
参考
Discussion