Block Rockin’ Codes

back with another one of those block rockin' codes

Go 1.3 で入った sync.Pool

Intro

先週 Go 1.3 が公開されましたが、 sync パッケージに Pool という API が追加されました。 ぱっと見なんだかよくわからなかったので調べてみました。

sync.Pool

sync.Pool

とりあえず、 GoDoc を訳したのでそれは最後に全部掲載します。 要点としては以下。

  • 従来独自に実装していた、 Free List を標準に入れた
  • Weak Map (弱参照)になっている
  • goroutine safe である

用途

一般的な FreeList の役割は、既に割り当て(Allocate)されたメモリが不要になった時、開放する代わりに List に持っておいて、メモリが必要になったらそこから取るというもの。(という理解) もし、不要になったメモリを捨てると、 GC の負荷が上がるし、再割り当ての負荷もあがるので、無駄が多くなる。 Go は C や C++ でやっていたような、システムに近い低レイヤのプログラムを書く用途でも使われるので、 GC はあるものの、こうした細かいメモリ管理を行いチューニングを行うこともあります。

実際に、標準でも fmt と regexp で、こうした用途の実装がなされていました。(go1.2)

多少実装はことなりますが、概ね以下の指針で実装されています。

  • []interface{} に格納
  • Get(), Put() を持つ
  • sync.Mutex などでロックを取る

fmt では具体的にはこんな感じ。

type cache struct {
    mu    sync.Mutex
    saved []interface{}
    new   func() interface{}
}

func (c *cache) put(x interface{}) {
    c.mu.Lock()
    if len(c.saved) < cap(c.saved) {
        c.saved = append(c.saved, x)
    }
    c.mu.Unlock()
}

func (c *cache) get() interface{} {
    c.mu.Lock()
    n := len(c.saved)
    if n == 0 {
        c.mu.Unlock()
        return c.new()
    }
    x := c.saved[n-1]
    c.saved = c.saved[0 : n-1]
    c.mu.Unlock()
    return x
}

func newCache(f func() interface{}) *cache {
    return &cache{saved: make([]interface{}, 0, 100), new: f}
}

var ppFree = newCache(func() interface{} { return new(pp) })

しかし、これにはいくつかの問題があります。

  • パッケージごとに同じような実装が必要
  • 気をつけて Lock-UnLock しないと goroutine-safe にならない
  • キャッシュから明示的に参照を消さないと GC されない

そこで、こうした機能自体を Go 標準に持ってはどうかという話を、前回の GoCon にも来日した Bradfiz が提案したのがことの始まりのようです。 そもそもこうした機能を標準に持つのか? 名前は Cache か Pool か? API はどうするか? など色々議論が重ねられた末に、今の形に落ち着き、今回 go1.3 に取り込まれたようです。

API

sync.Pool の API は非常にシンプルです。

http://golang.org/pkg/sync/#Pool

// キャッシュが切れたときのメモリ確保の方法を指定
type Pool struct {
    New func() interface{}
}

// キャッシュから取得
func (p *Pool) Get() interface{}

// キャッシュに戻す
func (p *Pool) Put(x interface{})

Put() したオブジェクトと、 Get() で取り出したオブジェクト間に参照関係はないものとして扱う必要があります。(例え 1 個入れて 1 個出したとしても)

WeakMap になっているため、 Get() で取り出したオブジェクトへの参照が切れれば、それは GC の対象になるようです。

実例としては、先ほど紹介した fmt のキャッシュ部分が 1.3 以降ではまさしくこの Pool を使って実装し直されています。

簡単に言うと、以下だけで終わり。あとは get(), put() で実装されていたものが Get(), Put() に変わっただけです。

var ppFree = sync.Pool{
    New: func() interface{} { return new(pp) },
}

探せてないけど、他にもありそう。

注意点というか、よくわかってないところとして、 GoDoc の以下が引っかかっている。

逆に、 寿命の短いオブジェクトの free list を管理する用途では、 Pool は向いていない。
なぜなら、そのシナリオではオーバーヘッドが減らないから。そうしたオブジェクトでは、独自に free list を実装した方が良い。

どの程度から、使い分けるべきなのだろうか。

翻訳

理解のために GoDoc 適当に翻訳したので張っておきます。

A Pool is a set of temporary objects that may be individually saved and retrieved.
Pool は個別に管理され回収される、一時オブジェクトのセット。


Any item stored in the Pool may be removed automatically at any time without notification. If the Pool holds the only reference when this happens, the item might be deallocated.
Pool にストアされたどんなアイテムも、通知なしに自動で消されることがある。
もし、 Pool が参照しか持たない場合にこれが起こると、アイテムはメモリから消去される。(deallocated)


A Pool is safe for use by multiple goroutines simultaneously.
Pool は複数 goroutine から同時に使用可能。


Pool's purpose is to cache allocated but unused items for later reuse, relieving pressure on the garbage collector. That is, it makes it easy to build efficient, thread-safe free lists. However, it is not suitable for all free lists.
Pool は、メモリに割り当てられているがもう不要なアイテムをキャッシュし、後で再利用することで、 GC の負荷を下げることを目的としている。
これにより、効率よくスレッドセーフな free list を作ることができる。しかし、すべての free list に適しているわけではない。


An appropriate use of a Pool is to manage a group of temporary items
silently shared among and potentially reused by concurrent independent clients of a package.
Pool の適した使い方は、並行する独立した、クライアントのパッケージから暗黙的に共有される、
再利用される可能性のある、複数の一時的なアイテムを管理すること。


Pool provides a way to amortize allocation overhead across many clients.
Pool は、複数のクライアントからのメモリ割り当てのオーバーヘッドを削減する方法を提供する。


An example of good use of a Pool is in the fmt package,
which maintains a dynamically-sized store of temporary output buffers.
The store scales under load (when many goroutines are actively printing) and shrinks when quiescent.
例えば、適した用途として、 動的にサイズを割り当てる一時的な出力バッファを管理する(標準出力としての) fmt パッケージがある。
複数の goroutine が頻繁に出力すれば、サイズは増えるし、少なければ縮小する。


On the other hand, a free list maintained as part of a short-lived object is not a suitable use for a Pool, since the overhead does not amortize well in that scenario. It is more efficient to have such objects implement their own free list.
逆に、 寿命の短いオブジェクトの free list を管理する用途では、 Pool は向いていない。
なぜなら、そのシナリオではオーバーヘッドが減らないから。
そうしたオブジェクトでは、独自に free list を実装した方が良い。

Outro

細かいチューニングが必要なプログラム以外では、あまり使うことは無いかもしれないですが、知っておいて損はなさそうです。