🙌

Goでベンチマーク!パフォーマンス改善のための第一歩

に公開

概要

Goを書いていると「この処理は遅いのか?速いのか?」と気になることが増えてきます。
そんなとき便利なのが Goの ベンチマーク機能 です。

今回は基本の使い方+よくある用途+便利なオプション に加え、
MutexChannel のパフォーマンス比較という実務にも役立つ例をご紹介します。


ベンチマークをやる理由

コードのパフォーマンス改善に取り組むとき、
「体感で遅い」「たぶんこっちが速そう」という思い込みにハマりがちです。

でもそれは危険。

💬 Rob Pike氏(Goの開発者の一人)の有名な言葉:
"Don’t tune for speed until you’ve measured, and even then don’t unless one part of the code overwhelms the rest."
👉「推測するな、計測せよ。」

つまり、実際に測ってから判断しろ ということです。


Goのベンチマークの使い方

Goの標準パッケージ testing には、ベンチマーク用の仕組みが組み込まれています。

  • ✅ 特別なツールは不要
  • ✅ 通常の go test コマンドで実行可能

ベンチマーク用の関数を用意しよう

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

💡 詳細

  • 関数名は Benchmark プレフィックスが必須
  • 引数は *testing.B
  • b.N 回ループして測定する(Goが自動調整)

testing.B の詳細はこちら
benchmark.go

具体例:スライスのAppend vs Copy

package mypackage

import "testing"

func BenchmarkAppend(b *testing.B) {
    for i := 0; i < b.N; i++ {
        s := []int{}
        s = append(s, 1, 2, 3, 4, 5)
    }
}

func BenchmarkCopy(b *testing.B) {
    for i := 0; i < b.N; i++ {
        src := []int{1, 2, 3, 4, 5}
        dst := make([]int, len(src))
        copy(dst, src)
    }
}

ベンチマークの実行方法

$ go test -bench=.

🧾 詳細

💡 詳細
-bench=. → すべての Benchmark 関数を実行
go test のサブコマンドとして動作

結果例

BenchmarkAppend-10   	10000000	       120 ns/op
BenchmarkCopy-10     	20000000	        80 ns/op

🧾 詳細

💡 詳細
何回ループしたか → 10000000
1回あたりの実行時間 → 120ns/op

用途例

  • アルゴリズムの比較(map vs slice など)
  • 圧縮/暗号/シリアライズなどの高速化
  • MutexとChannelの比較(ロック vs メッセージパッシング)
  • JSONライブラリの比較(標準 vs 高速版)
  • I/O 処理のオーバーヘッド確認
    👉 ボトルネック特定にも使える

オプション例

オプション 内容
-bench=Regexp 実行するベンチマークの名前指定
-benchmem メモリアロケーション数 / メモリ使用量も表示
-count=N N回繰り返して平均を取る(ノイズ低減)

例:メモリ消費も確認

$ go test -bench=. -benchmem

出力例:

BenchmarkAppend-10   	10000000	       120 ns/op	       64 B/op	       1 allocs/op

👉 B/op(1回の操作で何バイト確保するか)
👉 allocs/op(何回メモリ割当が発生したか)

Mutex vs Channel ベンチマーク例

ここが実務で とても役立つ! 例です。
同期処理に sync.Mutex を使うのと Channelを使うのとでは
パフォーマンスに大きな差が出ることが多いです。

ぜひ試してみましょう!

package mypackage

import (
	"sync"
	"testing"
)

func BenchmarkMutex(b *testing.B) {
	var mu sync.Mutex
	var counter int

	for i := 0; i < b.N; i++ {
		mu.Lock()
		counter++
		mu.Unlock()
	}
}

func BenchmarkChannel(b *testing.B) {
	ch := make(chan int, 1)
	ch <- 0

	for i := 0; i < b.N; i++ {
		val := <-ch
		val++
		ch <- val
	}
}

実行結果例

私の環境で試すと以下の結果になりました。

BenchmarkMutex-8   	 510459456	        2.255 ns/op
BenchmarkChannel-8 	 68742958	        19.21 ns/op

🧾 詳細

💡 Mutex
約5億回ループ
1回あたり 2.255ナノ秒
💡 Channel
約6800万回ループ
1回あたり 19.21ナノ秒

結果考察

  • 速さに関してはMutexに軍配
  • Channelは「goroutine間通信」「柔軟性」「安全性」はあるが、コストが大きい
  • 高頻度のロック操作にはMutexのほうが向いてそう

まとめ

  • Goのtestingパッケージはベンチマークが超簡単
  • 「推測するな、計測せよ」 → まずは測ってからチューニング
  • MutexとChannelでは明確な速度差がある → 用途に応じて選択
  • -benchmem や -count オプションを活用しよう

おまけ:もうひとつの格言

"First make it correct, then make it fast."
「まず正しく書け。速さはそのあとだ。」

Discussion