3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Goのベンチマークについての理解と読み方のまとめ

3
Posted at

Go college day4に参加してまいりました。今回の内容はパフォーマンスチューニングや単体テスト周りについて勉強しました。

私はgo test で単体テストを書くことはあっても、ベンチマークは今までちゃんと触ったことはあまりありませんでした。
ただ、パフォーマンスを見たいときに毎回 time.Now() で雑に測るだけだと、比較としてかなり弱いです。
そこで今回は、前回実際に書いたRaw SQL vs GOR の比較コードを使いながら、Goのベンチマークは何ができて、どう読むと勉強になるのかを整理してみました。

今回やりたいのは、アルゴリズムの理論比較というより、実装の違いがどの程度パフォーマンスに影響するのかを見ることです。
そのために見る指標は以下にしました。

  • スループット

  • 1回の処理あたりの時間

  • メモリアロケーション量

今回使うコードはこちらです。

5つのケースを計測する構成ですね。

そもそもGoのベンチマークとは何か

Goでは、*_test.go に BenchmarkXxx(*testing.B) という関数を書くと、go test -bench でベンチマークを実行できます。
testing パッケージのベンチマークは、対象処理を複数回実行して、ns/op のような形で結果を出してくれます。公式ドキュメントでも、ベンチマークは go test で実行する前提になっています。
たとえば基本形はこうです。

func BenchmarkSomething(b *testing.B) {
    for i := 0; i < b.N; i++ {
        // 計測したい処理
    }
}

この b.N は自分で決める回数ではなく、十分に安定した測定になるまで testing パッケージ側が調整してくれる回数です。
つまり1回だけ測るのではなくて、信頼しやすい数字になるまで何度も回してくれます。

今回のコードはどういう構成か

今回の benchmark_test.go では、最初のセットアップで以下をやっています。

1.MySQL に接続

2.テーブル作成

3.初回のみシード投入

4.sql.DB と gorm.DB を両方準備

5.ランダムに使う user_id / post_id のプールを作る

つまり、ただのマイクロベンチマークではなく、ある程度実運用に近いDBアクセスを比較するための土台を作っています。
対象ユースケースはこの5つです。

  • GetUserByID

  • ListPostsByUser

  • ListPostsWithCommentsCount

  • CreatePostWith2CommentsTx

  • UpdatePostStatus

この切り方が勉強になる理由は、単発のRead、複数件Read、Join集計、トランザクション付きWrite、単純Updateと、支配要因が変わるケースをちゃんと分けているからです。

今回のコードで勉強になるポイント1

  • b.Run でケースを切ると比較しやすい

たとえばBenchmarkGetUserByID では、1つのベンチマーク関数の中でraw_sqlとgormの2つを b.Run で分けています。

この形の良いところは、同じ前提条件・同じIDプールで2実装を比較できることです。

b.Run("raw_sql", func(b *testing.B) {
    ...
})

b.Run("gorm", func(b *testing.B) {
    ...
})

これをやらずに別のベンチ関数に分けることもできますが、今回みたいなA/B比較ではサブベンチにした方が見やすいです。
単純に「比較の土俵をそろえやすい」という意味で、かなり実務的だと思いました。

今回のコードで勉強になるポイント2

  • ReportAllocs を必ず見る

今回のリポジトリでは、各ケースで b.ReportAllocs() を呼んでいます。
これはメモリアロケーション統計を出すためのもので、-benchmem と同じ方向の情報を出す設定です。公式ドキュメントでも、ReportAllocs は malloc 統計を有効化するものと説明されています。

ベンチマーク結果を見ていると、初心者のうちは ns/op だけを見がちです。
でも実際には、B/op、allocs/opもかなり重要です。
特にORM比較みたいなケースだと、速度差がそこまで大きくなくても、allocs/op はかなり差が出ることがあります。
この差は、後で高頻度アクセスやGC負荷に効いてきます。だから 速いか遅いか だけで終わると、かなり浅い見方になります。

今回のコードで勉強になるポイント3
独自指標を載せられる

このコードでは reportOpsPerSec という関数を用意して、b.Elapsed() と b.N から ops/s を計算して出しています。
Goの testing.B には ReportMetric があり、独自メトリクスをベンチ結果に載せられます。

ベンチマークを書くときの注意点

  1. ベンチ対象以外をなるべく混ぜない

ベンチマークで見たいのがSQL実行なのに、毎回ランダム生成や巨大な前処理を入れると、何を測っているのか分からなくなります。

今回のコードでは、初期セットアップやIDプール作成を先にやっていて、ベンチループの中にはなるべく比較対象の処理を置いています。これはかなり大事です。

  1. 1ケースだけで一般化しない

GetUserByID だけ見て「やっぱりORMは全部遅い」と言うのは雑です。
逆に JOIN + GROUP BY だけ見て「差はない」と言うのも雑です。

今回のコードが良いのは、Read/Write/集計/トランザクションを分けているところです。
ベンチマークは、都合のいい1ケースで自説を補強する道具にすると一気に価値が下がると思います。

まとめ

今回ベンチマークを触ってみて思ったのは、Goのベンチマークは単に「何秒でした」を出すものではなくて、

  • 実装差を比較する

  • アロケーション差を見る

  • ユースケースごとに支配要因が変わることを確認する

  • 独自メトリクスまで含めて解釈しやすくする

ための道具だということでした。
特に今回の Raw SQL vs GORM のコードは、
b.Run でA/B比較している
ReportAllocs を見ている
ops/s を追加している
単発Readからトランザクションまでケースを分けている
ので、Goのベンチマークを勉強する題材としてかなり良いと思いました。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?