Goの標準パッケージには sync.Pool というものが用意されている。 これは、一度生成したものを使い回すような最適化を可能にするもので、 Pool.Newに生成処理を書くとGet() でプールから取得、Put() でプールへ返却ができる。
sync.Poolを使うことで700%(!)の高速化ができたという記事もあったりして、シンプルながら強力!
sync.Pool で生成する値の作り方
たとえば、[]byte
のスライスをPoolで使いまわしたいといった場合、素直に書くとこうなる。
pool := sync.Pool{New: func() interface{} { return make([]byte, 1024) }} buf := pool.Get().([]byte) ... pool.Put(buf)
しかし、Go公式リポジトリ内の sync.Pool の例を見てみると次のようなコメントが書かれていた。
var bufPool = sync.Pool{ New: func() interface{} { // The Pool's New function should generally only return pointer // types, since a pointer can be put into the return interface // value without an allocation: return new(bytes.Buffer) }, }
どうやら、interface{}
型を返す場合に、実体の型がポインタでないとアロケーションが発生してしまうため、
Pool.New で生成する値は基本的にポインタを返すべきらしい。ええーーそうなのか!
つまり、[]byte
を生成する場合は次のように変えたほうが良いということになる。
pool := sync.Pool{New: func() interface{} { buf := make([]byte, 1024) return &buf }} buf := pool.Get().(*[]byte) ... pool.Put(buf)
実際にベンチマークを取ってみる
本当にそうなのか気になったのでベンチマークを取ってみた。
- そもそもPoolを使わない場合
- 非ポインタを返すPoolを使った場合
- ポインタを返すPoolを場合
// スタックではなくヒープに乗せるためにここに宣言 var buf []byte func BenchmarkBufferWithoutPool(b *testing.B) { for i := 0; i < b.N; i++ { buf = make([]byte, 1024) } } func BenchmarkBytesWithValuePool(b *testing.B) { pool := sync.Pool{New: func() interface{} { return make([]byte, 1024) }} for i := 0; i < b.N; i++ { buf := pool.Get().([]byte) pool.Put(buf) } } func BenchmarkBytesWithPointerPool(b *testing.B) { pool := sync.Pool{New: func() interface{} { buf := make([]byte, 1024) return &buf }} for i := 0; i < b.N; i++ { buf := pool.Get().(*[]byte) pool.Put(buf) } }
BenchmarkBufferWithoutPool-8 7208229 139 ns/op 1024 B/op 1 allocs/op BenchmarkBytesWithValuePool-8 24608931 48.4 ns/op 32 B/op 1 allocs/op BenchmarkBytesWithPointerPool-8 69337044 16.9 ns/op 0 B/op 0 allocs/op
確かにポインタを使わない場合は、1 allocs/op と出て、ポインタを使った方は 0 allocs/op になった。なるほど。
感想
Goはあまりポインタとか値とか気にしなくても書けるな〜って思ってたら、こういった場所で差が出たりして難しさを感じた。
Go公式リポジトリのissueで「ポインタを返すべきとドキュメントに書くべきでは?」という提案がされたこともあったようだ。
これに対して「たとえ毎回allocationが発生してもpoolを使わない実装に比べるとパフォーマンスが良くなることもあるし、こういったものは細かい最適化テクニックだからアプリケーション実装側で各自ベンチマークを取ってやってくれ、ドキュメントには書かない」(雑訳)という形でスルーされていた。