golangのsliceパフォーマンスに関する小ネタ。
「いけない」というのは言い過ぎですがしない方が良いよレベルのお話。
NG例
例として引数で渡されてきたsliceから偶数の値だけ別のsliceに格納するような処理を書くとします。
偶数である数値の数は引数で渡された時点では不明であるためresultのlengthの初期値は0です。
func SliceFullExtension(samples []int32) []int32 {
var result = make([]int32, 0)
for _, s := range samples {
if s % 2 == 0 {
result = append(result, s)
}
}
return result
}
動作として問題ないですが上記のような書き方でsliceやmapを初期化してしまうと
appendの度に都度allocateが発生してしまい非常に効率が悪いです。
高速化その1: 最大値を定義した上で0にカットする
appendの度にsliceを拡張させないためには初めからsliceのメモリ確保をしてしまえば良いので
lengthに入りうる数の最大値を最初から定義してしまいます。
その上でsliceを0にカットします。
func SliceCut(samples []int32) []int32 {
var result = make([]int32, len(samples))[:0]
for _, s := range samples {
if s % 2 == 0 {
result = append(result, s)
}
}
return result
}
高速化その2: capacityを指定する
高速化その1のようなことをしなくても、実はmakeの第三引数にはcapacityを指定出来ます。
https://blog.golang.org/slices-intro
make([]T, length, capacity)
func SliceCap(samples []int32) []int32 {
var result = make([]int32, 0, len(samples))
for _, s := range samples {
if s % 2 == 0 {
result = append(result, s)
}
}
return result
}
こっちの方が良いですね。
ベンチマーク
どれくらい高速化出来るのかベンチマークを取ってみます。
func BenchmarkSlice(b *testing.B) {
b.Run("SliceFullExtension", func(b *testing.B) {
var samples = make([]int32, b.N)
for n := 0; n < b.N; n++ {
samples[n] = int32(n)
}
SliceFullExtension(samples)
})
b.Run("SliceCut", func(b *testing.B) {
var samples = make([]int32, b.N)
for n := 0; n < b.N; n++ {
samples[n] = int32(n)
}
SliceCut(samples)
})
b.Run("SliceCap", func(b *testing.B) {
var samples = make([]int32, b.N)
for n := 0; n < b.N; n++ {
samples[n] = int32(n)
}
SliceCap(samples)
})
}
BenchmarkSlice
BenchmarkSlice/SliceFullExtension
BenchmarkSlice/SliceFullExtension-8 215787544 5.36 ns/op
BenchmarkSlice/SliceCut
BenchmarkSlice/SliceCut-8 442801159 3.07 ns/op
BenchmarkSlice/SliceCap
BenchmarkSlice/SliceCap-8 522813318 3.40 ns/op
capacity指定版よりcut版の方が早いけど何度も実行すると逆転したりするので誤差の範囲です。
元々のcapacity指定しない版よりは2倍くらい早くなりました。