[インデックス 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.Writer
w
に接続します。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.Writer
w
を受け取ります。 - 内部的には、
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
: addReset
method toWriter
- 関連する
compress/flate
の変更:compress/flate: add Writer.Reset
- 関連する
compress/zlib
の変更:compress/zlib: add Writer.Reset
- このコミットのChangeList (CL):
参考にした情報源リンク
- Go issue #6138:
compress/gzip
: addReset
method 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/gzip
package (https://pkg.go.dev/compress/gzip) - Go Documentation:
compress/flate
package (https://pkg.go.dev/compress/flate) - Go Documentation:
hash/crc32
package (https://pkg.go.dev/hash/crc32) - Go Documentation:
io
package (https://pkg.go.dev/io) - Go Documentation:
bytes
package (https://pkg.go.dev/bytes) - Go Documentation:
fmt
package (https://pkg.go.dev/fmt) - Go Documentation:
testing
package (https://pkg.go.dev/testing) - Go Documentation:
bufio
package (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)