読者です 読者をやめる 読者になる 読者になる

Carpe Diem

備忘録。https://github.com/jun06t

sync.Poolを使って省コスト&高速化

Go

概要

golangにはsync.PoolというFreeListの仕組みがあります。
役割としては既に割り当てられたメモリが不要になった時、解放する代わりにListにとして保持しておいて、メモリが必要になったらそこから取るというものです。なのでGCコストやメモリのallocationコストを省くことが可能になります。
またgoroutine-safeであるので並列処理でも問題ありません。

環境

  • go 1.7.4

どんなところで使われている?

標準パッケージのfmtloggerライブラリなどでよく見ます。
一定の文字列を保持してから書き込む、という処理を何度も繰り返すので、毎回メモリを確保するよりFreeListを持って再利用することでGCコストやメモリのallocationコストを省いているのが分かります。

使い方

以下のような流れです。

  • sync.PoolのNewに、FreeListを新規作成するための関数を渡す
  • GetでFreeListを取得する
  • ↑でプールに空きがなければNewを呼んで新規で生成する
  • 使い終わったらリストの中身をリセットし、Putしてプールに戻す

コードとしては以下の感じです。

import "sync"

var cap = 10000

var globalPool = sync.Pool{
    New: func() interface{} {
        return make([]int, 0, cap)
    },
}

func someFunc() []int {
    list := globalPool.Get().([]int)

    for i := 0; i < 2*cap; i++ {
        list = append(list, i)
    }

    out := list
    // reset free list
    list = list[:0]
    // return free list to pool
    globalPool.Put(list)
    return out
}

比較

以下の3パターンで比較してみます。

  1. 毎回リストを生成
  2. グローバル変数にリストを保持
  3. sync.Poolにリストを保持

1. 毎回リストを生成

var cap = 10000

func someFunc1() {
    list := make([]int, 0, cap)

    for i := 0; i < 2*cap; i++ {
        list = append(list, i)
    }

    return
}

毎回sliceを生成するので無駄がある感じです。


2. グローバル変数にリストを保持

var cap = 10000

var globalList = make([]int, 0, cap)
var globalMutex = sync.Mutex{}

func someFunc2() {
    globalMutex.Lock()
    defer globalMutex.Unlock()

    for i := 0; i < 2*cap; i++ {
        globalList = append(globalList, i)
    }

    // reset free list
    globalList = globalList[:0]
    return
}

1だと毎回生成する無駄があるので自前でFreeListを用意した形です。
グローバルなのでそのままだとgoroutine-safeではないのでmutexを入れていますが、このリストを並行して利用することはできないのでスケールしません。


3. sync.Poolにリストを保持

var cap = 10000

var globalPool = sync.Pool{
    New: func() interface{} {
        return make([]int, 0, cap)
    },
}

func someFunc3() {
    list := globalPool.Get().([]int)

    for i := 0; i < 2*cap; i++ {
        list = append(list, i)
    }

    // reset free list
    list = list[:0]
    // return free list to pool
    globalPool.Put(list)
    return
}

sync.Poolで再利用する仕組みにしています。


ベンチマーク結果

$  go test -bench . -benchmem
testing: warning: no tests to run
BenchmarkSomeFunc1-8       20000         74012 ns/op      507904 B/op          4 allocs/op
BenchmarkSomeFunc2-8       30000         44044 ns/op           0 B/op          0 allocs/op
BenchmarkSomeFunc3-8       50000         32996 ns/op          42 B/op          1 allocs/op

速さはsync.Poolが、メモリ使用量・allocsはグローバル変数が少ないです。

グローバルに持ってるので、sync.Poolと違ってメモリアロケーションがカウントされないのだと思います。

しかしながら速さの差は並行で実行するとロックがないsync.Poolの方が何倍も早くすることができます。また毎回生成するケースと比べても非常にメモリ使用量が少ないです。

このようにsync.PoolによってFreeListを利用することで省コスト&高速化することが可能になります。

ソース