関連記事
GitHub - devlights/blog-summary: ブログ「いろいろ備忘録日記」のまとめ
概要
以下、自分用のメモです。忘れないようにここにメモメモ。。。
を読んでて、sync.Poolについての記載があったので試してみると
package main import ( "sync" "testing" ) func usePool(pool *sync.Pool) { buf := pool.Get().([]byte) clear(buf) buf = buf[:0] for i := 0; i < 1e6; i++ { buf = append(buf, 'a') } defer pool.Put(buf) // SA6002 } func BenchmarkXxx(b *testing.B) { b.Run("pool1", func(b *testing.B) { p := sync.Pool{ New: func() any { return make([]byte, 1e6) }, } b.ResetTimer() for i := 0; i < b.N; i++ { usePool(&p) } }) }
上のようなプログラムを書くと
defer pool.Put(buf)
の部分で、staticcheck が SA6002
という警告を出してきます。
なにこれ?ってなって、確認すると以下のような警告とのこと。
https://staticcheck.io/docs/checks#SA6002
内容を DeepL 先生で翻訳すると以下とのこと。
sync.Poolは、不必要なアロケーションを避け、ガベージ・コレクターの作業量を減らすために使われる。
インターフェイスを受け入れる関数にポインタでない値を渡す場合、その値をヒープに置く必要があり、これは追加の割り当てを意味する。スライスはsync.Poolsによく入れられるもので、3つのフィールド(長さ、容量、配列へのポインタ)を持つ構造体だ。余分なアロケーションを避けるためには、代わりにスライスへのポインタを格納する必要がある。
なるほど。無駄な割当を避けるために sync.Pool 使っているのだから、スライスをそのまま使うと追加の割当が発生するよと。
なので、スライスのポインタを使いなさいという指摘のようですね。
以下、試してみた結果です。
サンプル
SA6002の指摘内容に従って、スライスのポインタを利用するようにしたのが以下。
// usePoolWithPointer は、sync.Pool を利用したロジックです. // // プールの中には スライスのポインタ が格納されています。 // これは staticcheck の SA6002 の指摘に従ったやり方です。 // // # REFERENCES // - https://staticcheck.dev/docs/checks#SA6002 // - https://github.com/dominikh/go-tools/issues/1336 // - https://github.com/dominikh/go-tools/issues/302 func usePoolWithSlicePointer(p *sync.Pool) { bufPtr := p.Get().(*[]byte) buf := *bufPtr clear(buf) buf = buf[:0] for i := 0; i < BUF_LEN; i++ { buf = append(buf, 'a') } defer func() { // bufのスライスヘッダが編集によって変わってしまっている // 可能性(サイズや配列へのポインタが変更された場合に備えて)を考慮して // p.Put(&buf) とするのでは無く、元々プールに存在しているポインタである // bufPtr に上書きしてからプールに戻す。 // // REF: https://github.com/dominikh/go-tools/issues/1336#issuecomment-1331206290 *bufPtr = buf p.Put(bufPtr) }() // Use buf... }
普段、スライスのポインタなんて滅多に利用しないのでちょっと変な感じがしますが、SA6002は出なくなります。
ちなみに、書籍「効率的なGo」に記載されていたのと同じようにベンチマークとってみました。
package main import ( "sync" "testing" ) const ( BUF_LEN = 1e6 ) // useBuffer は、 []byte を利用したロジックです. func useBuffer(buf []byte) { clear(buf) buf = buf[:0] for i := 0; i < BUF_LEN; i++ { buf = append(buf, 'a') } // Use buf... } // usePoolWithPointer は、sync.Pool を利用したロジックです. // // プールの中には スライスのポインタ が格納されています。 // これは staticcheck の SA6002 の指摘に従ったやり方です。 // // # REFERENCES // - https://staticcheck.dev/docs/checks#SA6002 // - https://github.com/dominikh/go-tools/issues/1336 // - https://github.com/dominikh/go-tools/issues/302 func usePoolWithSlicePointer(p *sync.Pool) { bufPtr := p.Get().(*[]byte) buf := *bufPtr clear(buf) buf = buf[:0] for i := 0; i < BUF_LEN; i++ { buf = append(buf, 'a') } defer func() { // bufのスライスヘッダが編集によって変わってしまっている // 可能性(サイズや配列へのポインタが変更された場合に備えて)を考慮して // p.Put(&buf) とするのでは無く、元々プールに存在しているポインタである // bufPtr に上書きしてからプールに戻す。 // // REF: https://github.com/dominikh/go-tools/issues/1336#issuecomment-1331206290 *bufPtr = buf p.Put(bufPtr) }() // Use buf... } // usePoolWithSliceDirect は、sync.Pool を利用したロジックです. // // プールの中には スライス が格納されています。 // このコードは staticcheck だと SA6002 として警告されます。 // // ですが、余分な割当が行われるという点の警告であるので // 別段やってはいけない処理の書き方ではありません。 // // 実際にベンチマークを取ると SA6002 に従った書き方よりも // こちらの方が速度は速くなります。ただし、余計な割当が入る可能性がある。 // // REF: https://github.com/dominikh/go-tools/issues/1336 func usePoolWithSliceDirect(p *sync.Pool) { buf := p.Get().([]byte) clear(buf) buf = buf[:0] for i := 0; i < BUF_LEN; i++ { buf = append(buf, 'a') } defer p.Put(buf) //lint:ignore SA6002 非ポインタを意図的に利用しているので問題無し // Use buf... } func BenchmarkStaticCheckSA6002(b *testing.B) { b.Run("alloc", func(b *testing.B) { for i := 0; i < b.N; i++ { useBuffer(nil) } }) b.Run("buffer", func(b *testing.B) { buf := make([]byte, BUF_LEN) b.ResetTimer() for i := 0; i < b.N; i++ { useBuffer(buf) } }) b.Run("pool-sa6002-ok", func(b *testing.B) { pool := sync.Pool{ New: func() any { buf := make([]byte, BUF_LEN) return &buf }, } b.ResetTimer() for i := 0; i < b.N; i++ { usePoolWithSlicePointer(&pool) } }) b.Run("pool-sa6002-ng", func(b *testing.B) { pool := sync.Pool{ New: func() any { return make([]byte, BUF_LEN) }, } b.ResetTimer() for i := 0; i < b.N; i++ { usePoolWithSliceDirect(&pool) } }) }
$ task task: [default] go test -benchmem -run=^$ -bench . goos: linux goarch: amd64 pkg: github.com/devlights/try-golang/examples/singleapp/staticcheck_SA6002 cpu: AMD EPYC 7B13 BenchmarkStaticCheckSA6002/alloc-16 549 2159299 ns/op 5241629 B/op 33 allocs/op BenchmarkStaticCheckSA6002/buffer-16 1678 719659 ns/op 0 B/op 0 allocs/op BenchmarkStaticCheckSA6002/pool-sa6002-ok-16 1646 781101 ns/op 613 B/op 0 allocs/op BenchmarkStaticCheckSA6002/pool-sa6002-ng-16 2852 554210 ns/op 378 B/op 1 allocs/op PASS ok github.com/devlights/try-golang/examples/singleapp/staticcheck_SA6002 5.699s
SA6002の指摘を調整した pool-sa6002-ok-16
は、確かに 0 allocs/op
となっていますね。
でも、速度はSA6002が出るけど、普通にスライスをプールで利用するようにしている pool-sa6002-ng-16
の方が速い。
まあ、sync.Pool
を利用したい時というのは、速度じゃなくてメモリ割り当てを減らしたいときなので、ここはトレードオフですかね。
個人的には、sync.Pool
はあまり使うことが無くて、普通に必要サイズ分のバッファを最初に割り当てておいて、それを再利用する buffer-16
のやり方を一番使います。
参考情報
Goのおすすめ書籍
過去の記事については、以下のページからご参照下さい。
サンプルコードは、以下の場所で公開しています。