Go

Goでテンポラリなオブジェクトを再利用してGCを減らす方法

More than 3 years have passed since last update.

Goでは一応エスケープ解析をしていて、小さな値はnewで割り当ててもリテラルの構造体として書いても、スタックに割り付けることが可能ならスタックに割り付けることになっている。

スタックは関数呼び出しがリターンすると捨てられる(ゴミ扱いになる)領域なので、値をそこに割り当てるのは、ヒープに割り当てるのに比べて大変効率が良い。割り当てるのも速いし、GCがあとで回収する手間もかからない。

(ただしそういう値が関数からリターンした後に参照されては困るので、そういう値へのポインタなどが関数から逃げ出さないことをコンパイル時に静的に検証している ―― それがエスケープ解析。)

しかし大きな値(数百バイト〜)は、エスケープしないとしてもヒープに割り当てられてしまう。そうでなければスタックが成長しすぎて、それはそれでまた問題になるからだ。

頻繁に呼び出される関数で大きな値をテンポラリに使っていると、GCで回収されるゴミがどんどん発生してしまって、GCの負荷が大きくなってしまう。可能であれば前回割り当てた値を再利用して、あまりメモリをアロケートしないようにしたいところだ。


チャネルをバッファとして使う方法

一つのやり方は、チャネルを使用済みの値の入れ物として利用する方法だ。チャネルから値を取り出すことができれば、それを使えばいいし、取り出すことができなければ、仕方ないからnewしてその値を使えばよい。

// *T型の値を最大で1個保存しておくチャネル

c := make(*T, 1)

// T型の値を使いたいときはまずチャネルを見て利用できる値があるかを確認
var x *T
select {
case x = <-c:
// 前回使った値が取得できた。xは前の値のままなのでここで0クリアしておく
x.f1 = 0
x.f2 = 0
default:
// 前回使った値が取得できなかった。新たにアロケートする。
x = new(T)
}

// 使い終わったら元に戻す。チャネルが一杯でもブロックしたくないのでselectを使う。
select {
case c <- x:
default:
}


sync.Poolを使う

チャネルを使った方法を更に洗練させて、実行効率を向上させたのが、sync.Poolだ。使い方はドキュメントを呼んでもらうのが早いが、基本的なアイデアはチャネルを使うのと変わらない。