[インデックス 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 に関する一般的な情報 (コミットメッセージの背景理解のために参照しました)