[インデックス 17449] ファイルの概要
このコミットは、Go言語の標準ライブラリ compress/gzip パッケージに Writer.Reset メソッドを追加するものです。これにより、既存の gzip.Writer インスタンスを再利用して新しいGZIP圧縮ストリームを開始できるようになり、オブジェクトの再割り当て(アロケーション)を避けることでパフォーマンスを向上させます。
コミット
- コミットハッシュ:
db12f9d4e406dcab81b476e955c8e119112522fa - Author: Brad Fitzpatrick bradfitz@golang.org
- Date: Fri Aug 30 11:41:12 2013 -0700
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/db12f9d4e406dcab81b476e955c8e119112522fa
元コミット内容
compress/gzip: add Writer.Reset
compress/flate is https://golang.org/cl/12953048
compress/zlib is https://golang.org/cl/13171046
Update #6138
R=golang-dev, iant
CC=golang-dev
https://golang.org/cl/13435043
変更の背景
この変更の背景には、Go言語の標準ライブラリにおける圧縮ライブラリの効率性向上が挙げられます。特に、io.Writer インターフェースを実装する圧縮ライター(例: gzip.Writer, flate.Writer, zlib.Writer)は、ストリームの開始時に内部状態を初期化し、終了時にリソースを解放します。しかし、複数の圧縮操作を連続して行う場合、毎回新しいライターインスタンスを作成し、ガベージコレクションの対象とすることは、特に高頻度で圧縮が行われるアプリケーションにおいて、不要なアロケーションとGC負荷を発生させ、パフォーマンスのオーバーヘッドとなる可能性がありました。
コミットメッセージに Update #6138 とあるように、この変更はGo issue #6138「compress/gzip: add Reset method to Writer」に対応するものです。このIssueでは、gzip.Writer に Reset メソッドを追加することで、既存のライターを再利用し、新しい出力ストリームに接続できるようにすることが提案されていました。これにより、オブジェクトの再利用が可能になり、アロケーションを削減し、パフォーマンスを向上させることが期待されます。
また、このコミットは compress/flate (CL 12953048) と compress/zlib (CL 13171046) パッケージにおける同様の Reset メソッドの追加に続くものであり、Goの圧縮ライブラリ全体で一貫したAPIと最適化戦略を適用する流れの一部です。
前提知識の解説
compress/gzip パッケージ
compress/gzip パッケージは、RFC 1952 で定義されているGZIPデータ形式の読み書きを実装しています。GZIPは、主にDEFLATEアルゴリズム(compress/flate パッケージで提供)を使用してデータを圧縮し、CRC-32チェックサムと元のデータサイズを付加します。
io.Writer インターフェース
Go言語の io パッケージは、I/Oプリミティブを定義しています。io.Writer インターフェースは、Write([]byte) (int, error) メソッドを持つ型であり、バイトスライスを書き込む操作を抽象化します。gzip.Writer はこのインターフェースを実装しており、書き込まれたデータをGZIP形式で圧縮し、内部の io.Writer (コンストラクタで渡されたもの)に書き出します。
compress/flate パッケージ
compress/flate パッケージは、DEFLATE圧縮アルゴリズムを実装しています。GZIPやZLIBなどの高レベルな圧縮形式の基盤となります。flate.Writer は io.Writer インターフェースを実装し、データをDEFLATE形式で圧縮します。
hash/crc32 パッケージ
hash/crc32 パッケージは、CRC-32チェックサムアルゴリズムを実装しています。GZIP形式では、圧縮されていない元のデータのCRC-32チェックサムがヘッダーに格納され、データの整合性検証に使用されます。crc32.NewIEEE() は、IEEEポリノミアルに基づくCRC-32ハッシュを計算する hash.Hash32 インターフェースを返す関数です。
Reset メソッドの概念
Go言語の標準ライブラリでは、bufio.Reader や bufio.Writer など、一部のI/O関連の型に Reset メソッドが提供されています。このメソッドは、既存のインスタンスの内部状態をリセットし、新しい基盤となるI/Oストリーム(io.Reader や io.Writer)に接続し直すことを可能にします。これにより、新しいインスタンスをアロケートする代わりに、既存のインスタンスを再利用できるため、特にループ内で多数のI/O操作を行う場合に、ガベージコレクションの負荷を軽減し、パフォーマンスを向上させることができます。
技術的詳細
このコミットの主要な目的は、gzip.Writer の再利用を可能にする Reset メソッドを導入することです。これを実現するために、以下の変更が行われています。
-
Writer構造体の変更:wroteHeader boolフィールドが追加されました。これは、GZIPヘッダーが既に書き込まれたかどうかを追跡するためのフラグです。GZIPヘッダーはストリームの開始時に一度だけ書き込まれる必要があります。Reset後に新しいストリームを開始する際には、このフラグがリセットされる必要があります。- 既存のフィールドの順序が変更されていますが、これは機能的な変更ではなく、構造体のアライメントやメモリレイアウトの最適化に関連する可能性があります。
-
initヘルパー関数の導入:*Writer.init(w io.Writer, level int)という新しいプライベートヘルパー関数が導入されました。この関数は、Writerインスタンスの初期化ロジックをカプセル化します。NewWriterLevelとResetの両方から呼び出され、重複コードを排除し、初期化の一貫性を保証します。init関数内では、既存のdigest(CRC32ハッシュ) とcompressor(flate.Writer) が存在する場合、それらのResetメソッドを呼び出して状態をリセットし、再利用を試みます。存在しない場合は新しく作成します。これにより、内部オブジェクトも可能な限り再利用されます。
-
Writer.Resetメソッドの実装:func (z *Writer) Reset(w io.Writer)メソッドが追加されました。- このメソッドは、
z.init(w, z.level)を呼び出すことで、Writerの内部状態をリセットし、新しいio.Writerwに接続します。z.levelは元の圧縮レベルを保持します。 - これにより、
Writerインスタンスを破棄して再作成する代わりに、既存のインスタンスを新しい出力ストリームに対して再利用できるようになります。
-
Writeメソッドの変更:- GZIPヘッダーの遅延書き込みロジックが
z.compressor == nilから!z.wroteHeaderに変更されました。これは、Reset後にcompressorが再利用される可能性があるため、ヘッダー書き込みのトリガーをwroteHeaderフラグに依存させるように修正されたものです。 z.compressorがnilの場合にのみflate.NewWriterを呼び出すように変更され、Resetによってcompressorが再利用される場合に新しいflate.Writerが不必要に作成されるのを防ぎます。
- GZIPヘッダーの遅延書き込みロジックが
-
FlushおよびCloseメソッドの変更:- これらのメソッドでも、GZIPヘッダーの遅延書き込みの条件が
z.compressor == nilから!z.wroteHeaderに変更されました。これにより、Reset後もヘッダーが正しく書き込まれることが保証されます。
- これらのメソッドでも、GZIPヘッダーの遅延書き込みの条件が
-
テストケースの追加:
TestWriterResetという新しいテストケースがgzip_test.goに追加されました。このテストは、Writer.Resetメソッドが正しく機能し、リセット後に同じデータが正しく圧縮されることを検証します。
これらの変更により、gzip.Writer はより効率的に使用できるようになり、特に多数の小さなファイルをGZIP圧縮する場合など、アロケーションのオーバーヘッドが問題となるシナリオでパフォーマンス上のメリットをもたらします。
コアとなるコードの変更箇所
src/pkg/compress/gzip/gzip.go
Writer 構造体への wroteHeader フィールドの追加
--- a/src/pkg/compress/gzip/gzip.go
+++ b/src/pkg/compress/gzip/gzip.go
@@ -26,14 +26,15 @@ const (
// to its wrapped io.Writer.
type Writer struct {
Header
- w io.Writer
- level int
- compressor *flate.Writer
- digest hash.Hash32
- size uint32
- closed bool
- buf [10]byte
- err error
+ w io.Writer
+ level int
+ wroteHeader bool // 追加
+ compressor *flate.Writer
+ digest hash.Hash32
+ size uint32
+ closed bool
+ buf [10]byte
+ err error
}
init ヘルパー関数の追加
func (z *Writer) init(w io.Writer, level int) {
digest := z.digest
if digest != nil {
digest.Reset()
} else {
digest = crc32.NewIEEE()
}
compressor := z.compressor
if compressor != nil {
compressor.Reset(w)
}
*z = Writer{
Header: Header{
OS: 255, // unknown
},
w: w,
level: level,
digest: digest,
compressor: compressor,
}
}
NewWriterLevel の変更
init 関数を呼び出すように変更。
--- a/src/pkg/compress/gzip/gzip.go
+++ b/src/pkg/compress/gzip/gzip.go
@@ -62,14 +63,10 @@ func NewWriterLevel(w io.Writer, level int) (*Writer, error) {
if level < DefaultCompression || level > BestCompression {
return nil, fmt.Errorf("gzip: invalid compression level: %d", level)
}
- return &Writer{
- Header: Header{
- OS: 255, // unknown
- },
- w: w,
- level: level,
- digest: crc32.NewIEEE(),
- }, nil
+ z := new(Writer)
+ z.init(w, level)
+ return z, nil
}
Writer.Reset メソッドの追加
// Reset discards the Writer z's state and makes it equivalent to the
// result of its original state from NewWriter or NewWriterLevel, but
// writing to w instead. This permits reusing a Writer rather than
// allocating a new one.
func (z *Writer) Reset(w io.Writer) {
z.init(w, z.level)
}
Write メソッドの変更
ヘッダー書き込みの条件と flate.NewWriter の呼び出し条件を変更。
--- a/src/pkg/compress/gzip/gzip.go
+++ b/src/pkg/compress/gzip/gzip.go
@@ -138,7 +164,8 @@ func (z *Writer) Write(p []byte) (int, error) {
}
var n int
// Write the GZIP header lazily.
- if z.compressor == nil {
+ if !z.wroteHeader { // 変更
+ z.wroteHeader = true // 追加
z.buf[0] = gzipID1
z.buf[1] = gzipID2
z.buf[2] = gzipDeflate
@@ -183,7 +210,9 @@ func (z *Writer) Write(p []byte) (int, error) {
return n, z.err
}
}
- z.compressor, _ = flate.NewWriter(z.w, z.level)
+ if z.compressor == nil { // 追加
+ z.compressor, _ = flate.NewWriter(z.w, z.level)
+ }
}
z.size += uint32(len(p))
z.digest.Write(p)
Flush および Close メソッドの変更
ヘッダー書き込みの条件を変更。
--- a/src/pkg/compress/gzip/gzip.go
+++ b/src/pkg/compress/gzip/gzip.go
@@ -206,8 +235,11 @@ func (z *Writer) Flush() error {
if z.closed {
return nil
}
- if z.compressor == nil {
+ if !z.wroteHeader { // 変更
z.Write(nil)
+ if z.err != nil { // 追加
+ return z.err
+ }
}
z.err = z.compressor.Flush()
return z.err
@@ -222,7 +254,7 @@ func (z *Writer) Close() error {\n if z.closed {
return nil
}
z.closed = true
- if z.compressor == nil {
+ if !z.wroteHeader { // 変更
z.Write(nil)
if z.err != nil {
return z.err
src/pkg/compress/gzip/gzip_test.go
TestWriterReset テストケースの追加
func TestWriterReset(t *testing.T) {
buf := new(bytes.Buffer)
buf2 := new(bytes.Buffer)
z := NewWriter(buf)
msg := []byte("hello world")
z.Write(msg)
z.Close()
z.Reset(buf2) // Resetを呼び出し、出力先をbuf2に変更
z.Write(msg)
z.Close()
if buf.String() != buf2.String() {
t.Errorf("buf2 %q != original buf of %q", buf2.String(), buf.String())
}
}
コアとなるコードの解説
Writer 構造体への wroteHeader フィールドの追加
wroteHeader bool フィールドは、GZIPヘッダーが既に基盤となる io.Writer に書き込まれたかどうかを示すフラグです。GZIP形式では、ヘッダーは圧縮ストリームの最初に一度だけ書き込まれる必要があります。Reset メソッドが導入される前は、z.compressor == nil という条件がヘッダーの遅延書き込みのトリガーとして機能していました。しかし、Reset 後に flate.Writer (内部の compressor) が再利用される可能性があるため、compressor が nil でない場合でもヘッダーが書き込まれていない状況が発生し得ます。この wroteHeader フラグは、その問題を解決し、ヘッダーの書き込みを正確に制御するために導入されました。
init ヘルパー関数の導入
init 関数は、gzip.Writer の初期化ロジックを共通化するために導入されました。
NewWriterLevel(新しいWriterを作成する関数) とWriter.Reset(既存のWriterをリセットするメソッド) の両方から呼び出されます。digest(CRC32ハッシュ) のリセット/再利用:z.digestが既に存在する場合 (nilでない場合)、そのReset()メソッドを呼び出してハッシュの状態をクリアし、再利用します。存在しない場合は、新しくcrc32.NewIEEE()を呼び出して作成します。これにより、CRC32計算のためのアロケーションを削減します。compressor(flate.Writer) のリセット/再利用: 同様に、z.compressorが存在する場合、そのReset(w)メソッドを呼び出して、新しい出力先wに接続し、内部状態をリセットして再利用します。存在しない場合は、nilのままにしておき、最初のWrite呼び出し時にflate.NewWriterで作成されます。これにより、DEFLATE圧縮器のアロケーションも削減します。*z = Writer{...}の行は、現在のWriterインスタンスzのすべてのフィールドを新しいゼロ値または指定された値で上書きし、効果的に状態をリセットします。これにより、closedやerrなどの以前の状態がクリアされます。
Writer.Reset メソッドの実装
func (z *Writer) Reset(w io.Writer) は、gzip.Writer の公開APIとして追加されました。
- このメソッドは、引数として新しい出力先
io.Writerwを受け取ります。 - 内部的には、
z.init(w, z.level)を呼び出します。ここでz.levelは、Writerが最初に作成されたときの圧縮レベルを保持しています。これにより、Reset後も同じ圧縮レベルで動作が継続されます。 - このメソッドの導入により、アプリケーションは
gzip.Writerオブジェクトをプールし、必要に応じてResetを呼び出して再利用できるようになり、ガベージコレクションの頻度と負荷を大幅に削減できます。
Write メソッドの変更
- ヘッダーの遅延書き込み条件の変更: 以前は
if z.compressor == nil { ... }でGZIPヘッダーを書き込んでいましたが、Resetによってz.compressorが再利用される可能性があるため、この条件では不十分になりました。新しい条件if !z.wroteHeader { ... }は、wroteHeaderフラグがfalseの場合にのみヘッダーを書き込むようにします。ヘッダーが書き込まれた後、z.wroteHeader = trueが設定されます。 flate.NewWriterの呼び出し条件の変更:if z.compressor == nil { z.compressor, _ = flate.NewWriter(z.w, z.level) }というガードが追加されました。これにより、Resetによってz.compressorが既に初期化され、再利用されている場合には、不必要に新しいflate.Writerが作成されるのを防ぎます。
Flush および Close メソッドの変更
これらのメソッドでも、Write メソッドと同様に、GZIPヘッダーの遅延書き込みの条件が z.compressor == nil から !z.wroteHeader に変更されました。これにより、Flush や Close が呼び出された時点でまだヘッダーが書き込まれていない場合(例えば、データが全く書き込まれていない場合)、ヘッダーが正しく書き込まれることが保証されます。また、Flush メソッドでは、ヘッダー書き込み中にエラーが発生した場合にそのエラーを返すように修正されています。
関連リンク
- Go Issue #6138:
compress/gzip: addResetmethod toWriter - 関連する
compress/flateの変更:compress/flate: add Writer.Reset - 関連する
compress/zlibの変更:compress/zlib: add Writer.Reset - このコミットのChangeList (CL):
参考にした情報源リンク
- Go issue #6138:
compress/gzip: addResetmethod toWriter(https://github.com/golang/go/issues/6138) - Go CL 12953048:
compress/flate: add Writer.Reset(https://golang.org/cl/12953048) - Go CL 13171046:
compress/zlib: add Writer.Reset(https://golang.org/cl/13171046) - Go CL 13435043:
compress/gzip: add Writer.Reset(https://golang.org/cl/13435043) - RFC 1952: GZIP file format specification version 4.3 (https://www.rfc-editor.org/rfc/rfc1952)
- Go Documentation:
compress/gzippackage (https://pkg.go.dev/compress/gzip) - Go Documentation:
compress/flatepackage (https://pkg.go.dev/compress/flate) - Go Documentation:
hash/crc32package (https://pkg.go.dev/hash/crc32) - Go Documentation:
iopackage (https://pkg.go.dev/io) - Go Documentation:
bytespackage (https://pkg.go.dev/bytes) - Go Documentation:
fmtpackage (https://pkg.go.dev/fmt) - Go Documentation:
testingpackage (https://pkg.go.dev/testing) - Go Documentation:
bufiopackage (https://pkg.go.dev/bufio) -Resetメソッドの一般的な概念理解のため。 - Go Source Code:
src/pkg/compress/gzip/gzip.go(https://github.com/golang/go/blob/db12f9d4e406dcab81b476e955c8e119112522fa/src/pkg/compress/gzip/gzip.go) - Go Source Code:
src/pkg/compress/gzip/gzip_test.go(https://github.com/golang/go/blob/db12f9d4e406dcab81b476e955c8e119112522fa/src/pkg/compress/gzip/gzip_test.go)