こんにちは。アドベントカレンダー2日目ということで、本日は前から気になっていたGolangのスライスのパフォーマンスについて執筆します。
本内容では、Golangのスライスのベンチマークを比較する方法、その結果を共有します。
また、Golangのベンチマークの実行方法についても備忘録としてまとめます。
そもそもベンチマークとは?
恥ずかしながら、ベンチマークという単語すら聞いたことがありませんでした。
辞書でひくと:
比べる同類物との差が分かるような、数量的や質的な性質(の組)。
つまり今回はGolangのsliceのappendと事前に割り当てたときのパフォーマンスを定量的に評価するということになります。
実装
ファイルは _test.go という形式で保存してください。
今回は2つの方法を比較します:
-
sliceのlengthとcapを事前に割り当てて要素を代入していく方法 -
空sliceを定義して逐一appendしていく方法
slice_bench_test.go
package main
import "testing"
const Length = 100_000
func appendPreAllocate() []string {
slice := make([]string, 0, Length)
for j := 0; j < Length; j++ {
slice = append(slice, "elem")
}
return slice
}
func appendNoPreAllocate() []string {
slice := []string{}
for j := 0; j < Length; j++ {
slice = append(slice, "elem")
}
return slice
}
// 1. 割り当てる
func BenchmarkPreAllocate(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = appendPreAllocate()
}
}
// 2. 逐一追加
func BenchmarkAppendNoCap(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = appendNoPreAllocate()
}
}
実行方法
ターミナルで以下を実行します
go test -bench=. -benchmem
出力結果
goos: darwin
goarch: arm64
pkg: github.com/kikudesuyo/sandbox/golang-performance
cpu: Apple M3
BenchmarkPreAllocate-8 3686 319839 ns/op 1605642 B/op 1 allocs/op
BenchmarkAppendNoCap-8 327 3627580 ns/op 8923529 B/op 28 allocs/op
PASS
ok github.com/kikudesuyo/sandbox/golang-performance
結果の見方
ベンチマーク関数 実行回数 実行時間 (ns/op) 使用メモリ (B/op) 割当回数(allocs/op)
BenchmarkPreAllocate 3686 319,839 1,605,642 1
BenchmarkAppendNoCap 327 3,627,580 8,923,529 28
結果
- 実行時間は事前割当が約10倍高速
- 使用メモリは事前割当が約5倍少ない
- メモリ割当は事前割当が1回、逐次追加は28回
考察
- 要素数が事前に分かっている場合は、
appendを使うよりも事前にlengthとcapを定義する方が圧倒的に効率的 であることがわかった -
b.ReportAllocs()を使うことで、メモリ割当の回数やバイト数 も確認できるため、パフォーマンス改善の定量的な評価が可能である
本記事で身についた知識(備忘録)
- 要素数が分かっている場合はappendを使わないようにする(最重要!!)
-
Benchmark接頭辞をつけることでベンチマークを実行できる - ベンチマークは
_test.go内で記述し、go testコマンドで実行する - テスト関数の接頭辞には
Test,Benchmark,Exampleがある
さいごに
今回Benchmarkの使い方を理解しました。どうやら、Exampleもあることが分かり、今後はExampleの使い方も調べてまとめたいと思っています。
明日も何かしら投稿する予定なのでフォローしてお待ち下さい。