Golangにおいて、パフォーマンス低下の問題になりやすいのがメモリのアロケーションです。
例えば、スライスを固定長でmake
してアロケーションを一度で済ます場合と、append
で都度拡張する場合を比較すると、下記の差が出ます。
// make([]int, 0)で作成したスライスに、100回append
BenchmarkMake0-4 100000000 48.8 ns/op 49 B/op 0 allocs/op
// make([]int, 100)で作成したスライスに、100回append
BenchmarkMake100-4 100000000 14.6 ns/op 49 B/op 0 allocs/op
また、固定長で領域を確保した場合でも、当然ながらmake
でアロケーションの時間が発生します。
大容量のアロケーションが必要な場合や、頻繁に実行される処理の場合、アロケーションにかかる時間も極力短くしたいところです。
そういった速度面の問題を解決するため、fmt
やio/ioutil
の内部では、sync.Pool
を使用しています。
sync.Pool
を使用すると、一度使用した領域を破棄せずプールしておき、後で領域が必要となった際にアロケーション無しで再利用することができます。
使い方
まずはプールを管理するsync.Pool
型の変数を用意します。
そして、プールの在庫が無かった場合に領域を確保するための関数をNew
に定義します。
pool := sync.Pool{
New: func() interface{} {
return make([]int, 100)
},
}
プールの在庫を取得するには、Get
を使用します。
在庫が無かった場合には、New
で定義した関数で領域が新しく確保されます。
処理が終わった段階で、Put
で領域をプールに戻すことができます。
p := pool.Get().([]int)
// do something
pool.Put(p)
これらの仕組みはgoroutine safeなので、並行処理でも競合しません。
ただし、Get
でプールから取得した場合、過去にPut
した時点の値が格納されている点には注意が必要です(サンプル)。
また、プール中の領域は、通知なしで削除されることがあります。
計測
[]int
をcounts
要素分、確保した際のベンチマークを取得します。
検証コードは下記の通りです。
// go version go1.9.1 darwin/amd64
var counts = []int{0, 1, 10, 100, 1000, 10000, 100000, 1000000}
func BenchmarkWithoutPool(b *testing.B) {
for _, count := range counts {
b.Run(fmt.Sprintf("%d", count), func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = make([]int, count)
}
})
}
}
func BenchmarkWithPool(b *testing.B) {
for _, count := range counts {
b.Run(fmt.Sprintf("%d", count), func(b *testing.B) {
pool := sync.Pool{
New: func() interface{} {
return make([]int, count)
},
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
p := pool.Get().([]int)
pool.Put(p)
}
})
}
}
結果(sync.Pool
無効)
BenchmarkWithoutPool/0-4 200000000 8.79 ns/op 0 B/op 0 allocs/op
BenchmarkWithoutPool/1-4 50000000 25.2 ns/op 8 B/op 1 allocs/op
BenchmarkWithoutPool/10-4 30000000 44.8 ns/op 80 B/op 1 allocs/op
BenchmarkWithoutPool/100-4 10000000 209 ns/op 896 B/op 1 allocs/op
BenchmarkWithoutPool/1000-4 1000000 1687 ns/op 8192 B/op 1 allocs/op
BenchmarkWithoutPool/10000-4 100000 10979 ns/op 81920 B/op 1 allocs/op
BenchmarkWithoutPool/100000-4 10000 104683 ns/op 802816 B/op 1 allocs/op
BenchmarkWithoutPool/1000000-4 2000 814199 ns/op 8003584 B/op 1 allocs/op
結果(sync.Pool
有効)
BenchmarkWithPool/0-4 20000000 80.7 ns/op 32 B/op 1 allocs/op
BenchmarkWithPool/1-4 20000000 78.0 ns/op 32 B/op 1 allocs/op
BenchmarkWithPool/10-4 20000000 77.8 ns/op 32 B/op 1 allocs/op
BenchmarkWithPool/100-4 20000000 78.9 ns/op 32 B/op 1 allocs/op
BenchmarkWithPool/1000-4 20000000 78.3 ns/op 32 B/op 1 allocs/op
BenchmarkWithPool/10000-4 20000000 78.1 ns/op 32 B/op 1 allocs/op
BenchmarkWithPool/100000-4 20000000 80.5 ns/op 32 B/op 1 allocs/op
BenchmarkWithPool/1000000-4 20000000 81.9 ns/op 39 B/op 1 allocs/op
sync.Pool
が無効の場合、要素数の増加に伴い処理時間が長くなっています。
sync.Pool
が有効の場合、ほぼ一定の処理時間となり、プールが効いていることがわかります。
また、要素数が少ない場合は無効にした方が高速で、今回の場合では要素数10〜100の間で優劣が逆転しました。
少量のアロケーションであれば、make
の方が高速のようです。