S.Hayakawaをフォローする

Go言語のベンチマークについて

バックエンド

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