[インデックス 18455] ファイルの概要
このコミットは、Go言語の標準ライブラリ archive/zip
パッケージにおいて、圧縮されたZIPファイルを書き込む際に flate.Writer
インスタンスを再利用するように変更を加えるものです。これにより、大量のガベージ(不要なメモリ割り当て)の発生を防ぎ、特に大きなアーカイブを扱う際のパフォーマンスを向上させます。具体的には、sync.Pool
を利用して flate.Writer
をプールし、オブジェクトの再利用を促進しています。
コミット
- コミットハッシュ:
517f4a96837e345609aca6f5bdf1fbeb92c70647
- 作者: Brad Fitzpatrick bradfitz@golang.org
- コミット日時: 2014年2月11日 火曜日 11:41:25 -0800
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/517f4a96837e345609aca6f5bdf1fbeb92c70647
元コミット内容
archive/zip: re-use flate.Writers when writing compressed files
Prevents a ton of garbage. (Noticed this when writing large
Camlistore zip archives to Amazon Glacier)
Note that the Closer part of the io.WriteCloser is never given
to users. It's an internal detail of the package.
benchmark old ns/op new ns/op delta
BenchmarkCompressedZipGarbage 42884123 40732373 -5.02%
benchmark old allocs new allocs delta
BenchmarkCompressedZipGarbage 204 149 -26.96%
benchmark old bytes new bytes delta
BenchmarkCompressedZipGarbage 4397576 66744 -98.48%
LGTM=adg, rsc
R=adg, rsc
CC=golang-codereviews
https://golang.org/cl/54300053
変更の背景
この変更の主な背景は、archive/zip
パッケージで圧縮ファイルを書き込む際に発生する大量のガベージ(不要なメモリ割り当て)を削減することです。コミットメッセージにもあるように、特にCamlistore(分散ファイルシステム)がAmazon Glacier(アーカイブストレージサービス)に大量のZIPアーカイブを書き込むようなシナリオで、この問題が顕著になったようです。
flate.Writer
のようなオブジェクトは、圧縮処理のたびに新しく作成されると、そのたびにメモリが割り当てられ、使用後にガベージコレクションの対象となります。これが頻繁に繰り返されると、ガベージコレクタの負荷が増大し、アプリケーション全体のパフォーマンスに悪影響を及ぼします。
このコミットは、flate.Writer
インスタンスを再利用することで、新規割り当ての回数を減らし、結果としてガベージコレクションの頻度と負荷を軽減することを目的としています。ベンチマーク結果が示すように、この変更により、割り当てられるオブジェクト数(allocs)と割り当てられるバイト数(bytes)が大幅に削減され、実行時間(ns/op)も改善されています。
前提知識の解説
compress/flate
パッケージと flate.Writer
compress/flate
パッケージは、DEFLATE圧縮アルゴリズム(ZIP、gzip、PNGなどで使用される)の実装を提供します。
flate.Writer
は、io.Writer
インターフェースを実装しており、書き込まれたデータをDEFLATE形式で圧縮し、指定された出力 io.Writer
に書き出す役割を担います。
flate.NewWriter(w io.Writer, level int)
関数で新しい flate.Writer
インスタンスが作成されます。level
は圧縮レベルを指定します。
flate.Writer
は Close()
メソッドを持っており、これは圧縮ストリームを終了させ、バッファに残っているデータをすべてフラッシュするために呼び出す必要があります。
sync.Pool
sync.Pool
はGo言語の標準ライブラリ sync
パッケージで提供される型で、一時的なオブジェクトの再利用を可能にするためのプールです。
これは、頻繁に作成され、すぐに破棄されるようなオブジェクト(例: バッファ、特定の構造体インスタンス)の割り当てとガベージコレクションのオーバーヘッドを削減するために使用されます。
sync.Pool
は以下の主要なメソッドを持ちます。
Put(x interface{})
: オブジェクトをプールに戻します。Get() interface{}
: プールからオブジェクトを取得します。プールが空の場合、New
フィールドに設定された関数が呼び出されて新しいオブジェクトが作成されます。sync.Pool
はスレッドセーフであり、複数のゴルーチンから安全にアクセスできます。プールされたオブジェクトは、ガベージコレクションの際に自動的に破棄される可能性があるため、永続的なストレージとして使用すべきではありません。あくまで一時的なオブジェクトの再利用に特化しています。
ガベージコレクション (GC) とメモリ割り当て
Go言語は自動ガベージコレクションを備えています。プログラムが新しいオブジェクトを作成するたびに、メモリがヒープに割り当てられます。これらのオブジェクトが不要になると、ガベージコレクタがそれらを検出し、メモリを解放します。 メモリ割り当てが頻繁に行われると、ガベージコレクタがより頻繁に実行される必要があり、これがアプリケーションの実行を一時的に停止させる(ストップ・ザ・ワールド)原因となり、レイテンシの増加やスループットの低下につながることがあります。 オブジェクトの再利用は、新規割り当ての数を減らすことで、ガベージコレクタの作業量を軽減し、アプリケーションのパフォーマンスを向上させる一般的な最適化手法です。
io.WriteCloser
インターフェース
io.WriteCloser
は、io.Writer
と io.Closer
の両方のインターフェースを組み合わせたものです。
io.Writer
:Write([]byte) (n int, err error)
メソッドを持ち、データを書き込む機能を提供します。io.Closer
:Close() error
メソッドを持ち、リソースを解放する機能を提供します。flate.Writer
はio.WriteCloser
を実装しており、データを書き込み、最後にリソースをクリーンアップするためにClose()
を呼び出す必要があります。
技術的詳細
このコミットの核心は、archive/zip
パッケージが圧縮データを書き込む際に flate.Writer
の新しいインスタンスを毎回作成するのではなく、sync.Pool
を使用して既存のインスタンスを再利用するように変更した点です。
変更前は、Deflate
圧縮メソッドが要求されるたびに、flate.NewWriter(w, 5)
が直接呼び出され、新しい flate.Writer
が作成されていました。これは、特に多数の小さなファイルをZIPアーカイブに圧縮して追加する場合など、圧縮操作が頻繁に行われるシナリオで、大量の flate.Writer
オブジェクトが生成され、その結果としてガベージコレクションの負荷が増大するという問題を引き起こしていました。
変更後は、flateWriterPool
という sync.Pool
が導入されました。
-
newFlateWriter
関数:- この関数は、
io.Writer
を引数に取り、io.WriteCloser
を返します。 - まず
flateWriterPool.Get()
を呼び出して、プールから既存の*flate.Writer
インスタンスを取得しようとします。 - もしプールからインスタンスが取得できた場合(
ok
がtrue
)、そのインスタンスのReset(w)
メソッドを呼び出します。Reset
メソッドは、flate.Writer
を再初期化し、新しい出力io.Writer
に関連付け、内部状態をリセットすることで、新しいインスタンスを作成することなく再利用できるようにします。 - プールが空でインスタンスが取得できなかった場合、
flate.NewWriter(w, 5)
を呼び出して新しいflate.Writer
を作成します。 - 最終的に、取得または作成された
flate.Writer
は、pooledFlateWriter
という新しいラッパー構造体にラップされて返されます。
- この関数は、
-
pooledFlateWriter
構造体:- この構造体は、実際の
*flate.Writer
(fw
) と、並行アクセスから保護するためのsync.Mutex
(mu
) を保持します。 Write
メソッドは、ロックを取得してから内部のflate.Writer
のWrite
メソッドを呼び出します。これにより、複数のゴルーチンが同じpooledFlateWriter
インスタンスに同時に書き込もうとした場合の競合状態を防ぎます。Close
メソッドは、ロックを取得し、内部のflate.Writer
のClose()
を呼び出して圧縮ストリームを終了させます。その後、非常に重要な点として、flateWriterPool.Put(w.fw)
を呼び出して、使用済みのflate.Writer
インスタンスをプールに戻します。これにより、このインスタンスが将来の圧縮操作で再利用される可能性が生まれます。プールに戻した後、w.fw = nil
とすることで、pooledFlateWriter
がプールに戻されたflate.Writer
への参照をクリアし、誤って再利用されることを防ぎます。また、"Write after Close"
エラーを検出できるようにします。
- この構造体は、実際の
このメカニズムにより、flate.Writer
のインスタンスが頻繁に作成・破棄される代わりに、プールから取得・再利用されるようになります。これにより、新規のメモリ割り当てが大幅に削減され、ガベージコレクションの負荷が軽減され、結果としてパフォーマンスが向上します。ベンチマーク結果が示すように、割り当てられるオブジェクト数とバイト数が劇的に減少しているのは、このオブジェクトプーリングの効果です。
コミットメッセージにある「Note that the Closer part of the io.WriteCloser is never given to users. It's an internal detail of the package.」という記述は、pooledFlateWriter
の Close
メソッドが flate.Writer
をプールに戻すという内部的な処理を行うため、この Close
メソッドが外部のユーザーに直接公開されることはなく、パッケージ内部で適切に管理されるべき詳細であることを示しています。
コアとなるコードの変更箇所
変更は src/pkg/archive/zip/register.go
ファイルに対して行われました。
--- a/src/pkg/archive/zip/register.go
+++ b/src/pkg/archive/zip/register.go
@@ -6,6 +6,7 @@ package zip
import (
"compress/flate"
+ "errors"
"io"
"io/ioutil"
"sync"
@@ -21,12 +22,50 @@ type Compressor func(io.Writer) (io.WriteCloser, error)
// when they're finished reading.
type Decompressor func(io.Reader) io.ReadCloser
+var flateWriterPool sync.Pool
+
+func newFlateWriter(w io.Writer) io.WriteCloser {
+ fw, ok := flateWriterPool.Get().(*flate.Writer)
+ if ok {
+ fw.Reset(w)
+ } else {
+ fw, _ = flate.NewWriter(w, 5)
+ }
+ return &pooledFlateWriter{fw: fw}
+}
+
+type pooledFlateWriter struct {
+ mu sync.Mutex // guards Close and Write
+ fw *flate.Writer
+}
+
+func (w *pooledFlateWriter) Write(p []byte) (n int, err error) {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+ if w.fw == nil {
+ return 0, errors.New("Write after Close")
+ }
+ return w.fw.Write(p)
+}
+
+func (w *pooledFlateWriter) Close() error {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+ var err error
+ if w.fw != nil {
+ err = w.fw.Close()
+ flateWriterPool.Put(w.fw)
+ w.fw = nil
+ }
+ return err
+}
+
var (
mu sync.RWMutex // guards compressor and decompressor maps
compressors = map[uint16]Compressor{
Store: func(w io.Writer) (io.WriteCloser, error) { return &nopCloser{w}, nil },
- Deflate: func(w io.Writer) (io.WriteCloser, error) { return flate.NewWriter(w, 5) },
+ Deflate: func(w io.Writer) (io.WriteCloser, error) { return newFlateWriter(w), nil },
}
decompressors = map[uint16]Decompressor{
コアとなるコードの解説
flateWriterPool sync.Pool
の追加
sync.Pool
型のグローバル変数 flateWriterPool
が宣言されました。これが flate.Writer
インスタンスをプールするためのコンテナとなります。
newFlateWriter(w io.Writer) io.WriteCloser
関数の追加
この新しいヘルパー関数は、flate.Writer
インスタンスを取得・作成し、それを pooledFlateWriter
でラップして返します。
fw, ok := flateWriterPool.Get().(*flate.Writer)
: プールから*flate.Writer
を取得しようとします。if ok { fw.Reset(w) }
: 取得できた場合、Reset
メソッドを呼び出して、既存のflate.Writer
を新しい出力ストリームw
に関連付け、再利用可能な状態にします。else { fw, _ = flate.NewWriter(w, 5) }
: 取得できなかった場合(プールが空の場合)、新しくflate.NewWriter
を呼び出してインスタンスを作成します。圧縮レベルは5
に設定されています。return &pooledFlateWriter{fw: fw}
: 取得または作成したflate.Writer
をpooledFlateWriter
のインスタンスに埋め込み、そのポインタを返します。
pooledFlateWriter
構造体の追加
この構造体は、flate.Writer
のラッパーとして機能し、io.WriteCloser
インターフェースを実装します。
mu sync.Mutex
:Write
とClose
メソッドの並行アクセスを保護するためのミューテックスです。これにより、複数のゴルーチンが同時に同じpooledFlateWriter
インスタンスにアクセスしても安全性が保たれます。fw *flate.Writer
: 実際に圧縮処理を行うflate.Writer
インスタンスへのポインタです。
(w *pooledFlateWriter) Write(p []byte) (n int, err error)
メソッドの実装
w.mu.Lock()
とdefer w.mu.Unlock()
: メソッドの実行中、ミューテックスをロックし、メソッド終了時にアンロックします。if w.fw == nil { return 0, errors.New("Write after Close") }
:Close
が既に呼び出され、fw
がnil
に設定されている場合、エラーを返します。これは、プールに戻されたflate.Writer
を誤って使用することを防ぐためのガードです。return w.fw.Write(p)
: 内部のflate.Writer
のWrite
メソッドを呼び出し、データを圧縮して出力します。
(w *pooledFlateWriter) Close() error
メソッドの実装
w.mu.Lock()
とdefer w.mu.Unlock()
: メソッドの実行中、ミューテックスをロックし、メソッド終了時にアンロックします。if w.fw != nil
:fw
がnil
でない場合(まだクローズされていない場合)に処理を行います。err = w.fw.Close()
: 内部のflate.Writer
のClose()
メソッドを呼び出します。これにより、圧縮ストリームが適切に終了し、バッファ内の残りのデータがフラッシュされます。flateWriterPool.Put(w.fw)
: この行が最も重要です。 使用済みのflate.Writer
インスタンスをflateWriterPool
に戻します。これにより、このインスタンスが将来の圧縮操作で再利用される準備が整います。w.fw = nil
:flate.Writer
インスタンスをプールに戻した後、pooledFlateWriter
のfw
フィールドをnil
に設定します。これにより、このpooledFlateWriter
インスタンスが誤ってプールに戻されたflate.Writer
を参照し続けることを防ぎ、また、Write after Close
のチェックを可能にします。return err
: 内部のflate.Writer
のClose()
から返されたエラーを返します。
compressors
マップの変更
Deflate: func(w io.Writer) (io.WriteCloser, error) { return flate.NewWriter(w, 5) },
がDeflate: func(w io.Writer) (io.WriteCloser, error) { return newFlateWriter(w), nil },
に変更されました。
これにより、Deflate
圧縮方式が選択された際に、直接 flate.NewWriter
を呼び出す代わりに、新しく追加された newFlateWriter
ヘルパー関数が呼び出されるようになります。このヘルパー関数が sync.Pool
を介して flate.Writer
の再利用を管理するため、ガベージの削減とパフォーマンスの向上が実現されます。
関連リンク
- Go言語
sync.Pool
のドキュメント: https://pkg.go.dev/sync#Pool - Go言語
compress/flate
のドキュメント: https://pkg.go.dev/compress/flate - Go言語
archive/zip
のドキュメント: https://pkg.go.dev/archive/zip - Go言語のガベージコレクションについて (公式ブログ): https://go.dev/blog/go15gc (このコミットより後の記事ですが、GCの概念理解に役立ちます)
参考にした情報源リンク
- コミット情報:
/home/orange/Project/comemo/commit_data/18455.txt
- GitHub上のコミットページ: https://github.com/golang/go/commit/517f4a96837e345609aca6f5bdf1fbeb92c70647
- Go言語の公式ドキュメント (上記「関連リンク」に記載の各パッケージドキュメント)
sync.Pool
の一般的な使用例とメリットに関するGoコミュニティの議論や記事 (具体的なURLは省略しますが、概念理解のために参照しました)- DEFLATE圧縮アルゴリズムに関する一般的な知識 (具体的なURLは省略しますが、
flate
パッケージの背景理解のために参照しました) - Camlistore と Amazon Glacier に関する一般的な情報 (コミットメッセージの背景理解のために参照しました)