[インデックス 17473] ファイルの概要
このコミットは、Go言語の標準ライブラリ compress/zlib
パッケージの Writer
型に Reset
メソッドを追加するものです。これにより、既存の zlib.Writer
インスタンスを再利用して、新しい io.Writer
に圧縮データを書き込めるようになります。これは、特に多数の圧縮操作を行う際に、オブジェクトの再割り当てと初期化のオーバーヘッドを削減し、パフォーマンスを向上させることを目的としています。
コミット
commit 86c0cf10cb8d679039c2d51458435ff221352f81
Author: Rémy Oudompheng <oudomphe@phare.normalesup.org>
Date: Thu Sep 5 21:50:47 2013 +0200
compress/zlib: add Reset method to Writer.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/13171046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/86c0cf10cb8d679039c2d51458435ff221352f81
元コミット内容
このコミットは、Go言語の compress/zlib
パッケージにおいて、Writer
型に Reset
メソッドを追加することを目的としています。Reset
メソッドは、Writer
の内部状態を初期状態に戻し、新しい出力先 io.Writer
を設定できるようにします。これにより、Writer
オブジェクトを再利用することが可能になり、特に連続して複数のデータを圧縮するようなシナリオにおいて、新しい Writer
を毎回作成する際のオーバーヘッド(メモリ割り当てや初期化)を削減し、効率を向上させることができます。
変更の背景
Go言語の compress/zlib
パッケージは、Zlib形式の圧縮データストリームを書き込むための機能を提供します。これまでの実装では、一度 zlib.Writer
を作成してデータを圧縮し終えると、その Writer
は再利用できませんでした。新しい圧縮ストリームを開始するには、常に NewWriterLevel
または NewWriterLevelDict
を呼び出して新しい Writer
インスタンスを作成する必要がありました。
しかし、多くのアプリケーションでは、同じ設定(圧縮レベルや辞書)で繰り返しデータを圧縮するユースケースが存在します。例えば、HTTPサーバーが複数のクライアントに圧縮されたレスポンスを返す場合や、ログファイルを定期的に圧縮する場合などです。このようなシナリオでは、毎回新しい Writer
を作成することは、ガベージコレクションの負荷を増やし、パフォーマンスに悪影響を与える可能性があります。
Reset
メソッドの導入は、この問題を解決するために提案されました。Reset
メソッドを使用することで、既存の Writer
インスタンスを「リセット」し、新しい出力先 io.Writer
に向けて再利用できるようになります。これにより、オブジェクトの再割り当てが不要になり、メモリ使用量の削減とパフォーマンスの向上が期待されます。これは、Go言語の他の圧縮パッケージ(例: compress/gzip
や compress/flate
)にも同様の Reset
メソッドが存在することと整合性を保つものでもあります。
前提知識の解説
Zlib 圧縮
Zlibは、データ圧縮のためのソフトウェアライブラリであり、RFC 1950 (Zlib Format) と RFC 1951 (Deflate Compressed Data Format) で定義されたデータ形式を実装しています。Zlibは、Deflateアルゴリズム(LZ77とハフマン符号化の組み合わせ)を使用してデータを圧縮し、Adler-32チェックサムを使用してデータの整合性を保証します。Go言語の compress/zlib
パッケージは、このZlib形式のデータを読み書きするための機能を提供します。
io.Writer
インターフェース
Go言語の io.Writer
インターフェースは、データを書き込むための基本的な抽象化を提供します。これは、Write([]byte) (n int, err error)
メソッドを持つ型によって実装されます。ファイル、ネットワーク接続、メモリバッファなど、様々な出力先がこのインターフェースを実装できます。zlib.Writer
は、この io.Writer
インターフェースを実装しており、また、内部で別の io.Writer
を受け取り、そこに圧縮されたデータを書き込みます。
flate.NewWriterDict
compress/flate
パッケージは、Deflate圧縮アルゴリズムを直接扱うための機能を提供します。flate.NewWriterDict
関数は、指定された io.Writer
と圧縮レベル、そしてオプションの辞書(dict
)を使用して、新しいDeflateライターを作成します。zlib.Writer
は、内部でこの flate.Writer
を使用して実際の圧縮処理を行います。
adler32
チェックサム
Adler-32は、データの整合性をチェックするために使用されるチェックサムアルゴリズムです。Zlib形式では、圧縮されたデータの最後にAdler-32チェックサムが付加され、データの破損を検出するために使用されます。hash/adler32
パッケージは、Go言語でAdler-32チェックサムを計算するための機能を提供します。zlib.Writer
は、圧縮中にデータのAdler-32チェックサムを計算し、Zlibフッターに含めます。
コンプレッサーのリセットの概念
多くの圧縮ライブラリでは、パフォーマンス向上のために、圧縮器(コンプレッサー)のインスタンスを再利用する機能が提供されています。これは、新しい圧縮ストリームを開始する際に、圧縮器の内部状態を初期化し、新しい入力/出力ストリームに接続し直すことを意味します。これにより、メモリの再割り当てや複雑な初期化処理を回避し、効率的な連続圧縮を可能にします。
技術的詳細
このコミットの主要な変更点は、compress/zlib/writer.go
に (*Writer) Reset(w io.Writer)
メソッドが追加されたことです。
Reset
メソッドの実装
Reset
メソッドは以下の処理を行います。
- 出力先
io.Writer
の更新:z.w = w
により、zlib.Writer
がデータを書き込む先のio.Writer
を新しいものに設定します。 - 圧縮レベルと辞書の維持:
z.level
とz.dict
は変更されません。これは、Reset
が同じ圧縮設定で再利用されることを意図しているためです。 - 内部コンプレッサーのリセット:
z.compressor != nil
の場合、つまり内部のflate.Writer
が既に存在する場合、z.compressor.Reset(w)
を呼び出してflate.Writer
の状態をリセットし、新しい出力先w
に接続します。これにより、Deflate圧縮器も再利用されます。 - Adler-32ダイジェストのリセット:
z.digest != nil
の場合、つまりAdler-32チェックサム計算器が既に存在する場合、z.digest.Reset()
を呼び出してチェックサム計算器の状態を初期化します。これにより、新しいデータのチェックサムをゼロから計算できるようになります。 - エラー状態のクリア:
z.err = nil
により、以前の圧縮操作で発生したエラー状態をクリアします。 - スクラッチバッファのクリア:
z.scratch = [4]byte{}
により、内部で使用される一時的なバッファをクリアします。 - ヘッダー書き込みフラグのリセット:
z.wroteHeader = false
により、Zlibヘッダーがまだ書き込まれていない状態に戻します。これにより、次のWrite
呼び出し時に新しいZlibヘッダーが書き込まれるようになります。
writeHeader
メソッドの変更
writeHeader
メソッドにも変更が加えられています。
変更前:
z.compressor, err = flate.NewWriterDict(z.w, z.level, z.dict)
if err != nil {
return err
}
z.digest = adler32.New()
変更後:
if z.compressor == nil {
// Initialize deflater unless the Writer is being reused
// after a Reset call.
z.compressor, err = flate.NewWriterDict(z.w, z.level, z.dict)
if err != nil {
return err
}
z.digest = adler32.New()
}
この変更は、Reset
メソッドが呼び出された後に writeHeader
が実行される場合、z.compressor
が既に存在している可能性があるためです。Reset
メソッドは z.compressor.Reset(w)
を呼び出すことで既存のコンプレッサーを再利用するため、writeHeader
で再度 flate.NewWriterDict
を呼び出して新しいコンプレッサーを作成する必要はありません。if z.compressor == nil
のチェックにより、Reset
が使用された場合は新しいコンプレッサーの作成をスキップし、既存のコンプレッサーを再利用するロジックが適切に機能するようになります。
テストケースの追加
compress/zlib/writer_test.go
には、Reset
メソッドの動作を検証するための新しいテスト関数 testFileLevelDictReset
と TestWriterReset
が追加されています。これらのテストは、様々な圧縮レベルと辞書設定でデータを圧縮し、その後 Reset
を呼び出して同じデータを再度圧縮し、両方の圧縮結果が同一であることを確認します。これにより、Reset
メソッドが正しく機能し、一貫した圧縮出力を生成することが保証されます。
コアとなるコードの変更箇所
src/pkg/compress/zlib/writer.go
--- a/src/pkg/compress/zlib/writer.go
+++ b/src/pkg/compress/zlib/writer.go
@@ -70,6 +70,23 @@ func NewWriterLevelDict(w io.Writer, level int, dict []byte) (*Writer, error) {
}, nil
}
+// Reset clears the state of the Writer z such that it is equivalent to its
+// initial state from NewWriterLevel or NewWriterLevelDict, but instead writing
+// to w.
+func (z *Writer) Reset(w io.Writer) {
+ z.w = w
+ // z.level and z.dict left unchanged.
+ if z.compressor != nil {
+ z.compressor.Reset(w)
+ }
+ if z.digest != nil {
+ z.digest.Reset()
+ }
+ z.err = nil
+ z.scratch = [4]byte{}
+ z.wroteHeader = false
+}
+
// writeHeader writes the ZLIB header.
func (z *Writer) writeHeader() (err error) {
z.wroteHeader = true
@@ -111,11 +128,15 @@ func (z *Writer) writeHeader() (err error) {
return err
}
}
- z.compressor, err = flate.NewWriterDict(z.w, z.level, z.dict)
- if err != nil {
- return err
+ if z.compressor == nil {
+ // Initialize deflater unless the Writer is being reused
+ // after a Reset call.
+ z.compressor, err = flate.NewWriterDict(z.w, z.level, z.dict)
+ if err != nil {
+ return err
+ }
+ z.digest = adler32.New()
}
- z.digest = adler32.New()
return nil
}
src/pkg/compress/zlib/writer_test.go
--- a/src/pkg/compress/zlib/writer_test.go
+++ b/src/pkg/compress/zlib/writer_test.go
@@ -89,6 +89,56 @@ func testLevelDict(t *testing.T, fn string, b0 []byte, level int, d string) {
}
}
+func testFileLevelDictReset(t *testing.T, fn string, level int, dict []byte) {
+ var b0 []byte
+ var err error
+ if fn != "" {
+ b0, err = ioutil.ReadFile(fn)
+ if err != nil {
+ t.Errorf("%s (level=%d): %v", fn, level, err)
+ return
+ }
+ }
+
+ // Compress once.
+ buf := new(bytes.Buffer)
+ var zlibw *Writer
+ if dict == nil {
+ zlibw, err = NewWriterLevel(buf, level)
+ } else {
+ zlibw, err = NewWriterLevelDict(buf, level, dict)
+ }
+ if err == nil {
+ _, err = zlibw.Write(b0)
+ }
+ if err == nil {
+ err = zlibw.Close()
+ }
+ if err != nil {
+ t.Errorf("%s (level=%d): %v", fn, level, err)
+ return
+ }
+ out := buf.String()
+
+ // Reset and comprses again.
+ buf2 := new(bytes.Buffer)
+ zlibw.Reset(buf2)
+ _, err = zlibw.Write(b0)
+ if err == nil {
+ err = zlibw.Close()
+ }
+ if err != nil {
+ t.Errorf("%s (level=%d): %v", fn, level, err)
+ return
+ }
+ out2 := buf2.String()
+
+ if out2 != out {
+ t.Errorf("%s (level=%d): different output after reset (got %d bytes, expected %d",
+ fn, level, len(out2), len(out))
+ }
+}
+
func TestWriter(t *testing.T) {
for i, s := range data {
\tb := []byte(s)
@@ -122,6 +172,21 @@ func TestWriterDict(t *testing.T) {\n \t}\n }\n \n+func TestWriterReset(t *testing.T) {\n+\tconst dictionary = \"0123456789.\"\n+\tfor _, fn := range filenames {\n+\t\ttestFileLevelDictReset(t, fn, NoCompression, nil)\n+\t\ttestFileLevelDictReset(t, fn, DefaultCompression, nil)\n+\t\ttestFileLevelDictReset(t, fn, NoCompression, []byte(dictionary))\n+\t\ttestFileLevelDictReset(t, fn, DefaultCompression, []byte(dictionary))\n+\t\tif !testing.Short() {\n+\t\t\tfor level := BestSpeed; level <= BestCompression; level++ {\n+\t\t\t\ttestFileLevelDictReset(t, fn, level, nil)\n+\t\t\t}\n+\t\t}\n+\t}\n+}\n+\n func TestWriterDictIsUsed(t *testing.T) {\n \tvar input = []byte(\"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\")\n \tvar buf bytes.Buffer\n```
## コアとなるコードの解説
### `Writer.Reset` メソッド
`Writer` 型に新しく追加された `Reset` メソッドは、`zlib.Writer` インスタンスを再利用可能にするための中心的な機能です。
```go
func (z *Writer) Reset(w io.Writer) {
z.w = w
// z.level and z.dict left unchanged.
if z.compressor != nil {
z.compressor.Reset(w)
}
if z.digest != nil {
z.digest.Reset()
}
z.err = nil
z.scratch = [4]byte{}
z.wroteHeader = false
}
z.w = w
: これは、zlib.Writer
が圧縮データを書き込む先のio.Writer
を、引数w
で指定された新しい出力先に変更します。z.level
とz.dict
は変更されません。これは、Reset
が同じ圧縮設定(圧縮レベルと辞書)でWriter
を再利用することを前提としているためです。if z.compressor != nil { z.compressor.Reset(w) }
:z.compressor
は内部で使用されるflate.Writer
のインスタンスです。もし既に存在していれば、そのReset
メソッドを呼び出して、flate.Writer
の内部状態をリセットし、新しい出力先w
に接続し直します。これにより、Deflate圧縮器自体も再利用され、新しいインスタンスの生成が不要になります。if z.digest != nil { z.digest.Reset() }
:z.digest
はAdler-32チェックサムを計算するためのadler32.Hash
インスタンスです。もし存在していれば、そのReset
メソッドを呼び出して、チェックサム計算器の状態を初期化します。これにより、新しい圧縮ストリームのチェックサムをゼロから計算できます。z.err = nil
: 以前の圧縮操作で発生した可能性のあるエラー状態をクリアします。z.scratch = [4]byte{}
: 内部で使用される一時的な4バイトのバッファをクリアします。z.wroteHeader = false
: Zlibヘッダーがまだ書き込まれていない状態を示すフラグをfalse
に設定します。これにより、Reset
後に初めてデータが書き込まれる際に、新しいZlibヘッダーが適切に生成され、出力ストリームの先頭に書き込まれることが保証されます。
writeHeader
メソッドの変更点
writeHeader
メソッドは、Zlibヘッダーを書き込む際に内部の flate.Writer
と adler32.Hash
を初期化する役割を担っています。Reset
メソッドの導入に伴い、この部分に条件分岐が追加されました。
if z.compressor == nil {
// Initialize deflater unless the Writer is being reused
// after a Reset call.
z.compressor, err = flate.NewWriterDict(z.w, z.level, z.dict)
if err != nil {
return err
}
z.digest = adler32.New()
}
if z.compressor == nil
: この条件分岐が追加されたことで、z.compressor
がnil
の場合にのみ、新しいflate.Writer
とadler32.Hash
インスタンスが作成されるようになりました。Reset
メソッドが呼び出された場合、z.compressor
は既に存在し、z.compressor.Reset(w)
によってリセットされています。したがって、このif
ブロック内のコードは実行されず、既存のflate.Writer
とadler32.Hash
が再利用されます。- これにより、
Reset
メソッドによるオブジェクトの再利用が正しく機能し、不要なオブジェクトの再生成が回避されます。
テストコードの追加
writer_test.go
に追加された testFileLevelDictReset
関数は、Reset
メソッドの機能と正確性を検証するためのものです。
func testFileLevelDictReset(t *testing.T, fn string, level int, dict []byte) {
// ... (ファイル読み込み、初回圧縮) ...
// Reset and comprses again.
buf2 := new(bytes.Buffer)
zlibw.Reset(buf2) // ここでResetを呼び出す
_, err = zlibw.Write(b0)
if err == nil {
err = zlibw.Close()
}
// ... (エラーチェック) ...
out2 := buf2.String()
if out2 != out { // 初回圧縮とReset後の圧縮結果が同一であることを確認
t.Errorf("%s (level=%d): different output after reset (got %d bytes, expected %d",
fn, level, len(out2), len(out))
}
}
このテストは、まず一度データを圧縮し、その結果を out
に保存します。次に、同じ zlib.Writer
インスタンスに対して Reset
メソッドを呼び出し、新しい bytes.Buffer
に向けて同じデータを再度圧縮します。最後に、初回圧縮の結果 out
と Reset
後の圧縮結果 out2
が完全に一致することを確認します。これにより、Reset
メソッドが Writer
の状態を正しく初期化し、一貫した圧縮出力を生成できることが保証されます。
TestWriterReset
関数は、この testFileLevelDictReset
を様々なファイル、圧縮レベル、および辞書設定で繰り返し呼び出し、広範なテストカバレッジを提供します。
関連リンク
- Go
compress/zlib
パッケージドキュメント: https://pkg.go.dev/compress/zlib - Go
compress/flate
パッケージドキュメント: https://pkg.go.dev/compress/flate - Go
hash/adler32
パッケージドキュメント: https://pkg.go.dev/hash/adler32 - RFC 1950 - Zlib Compressed Data Format Specification version 3.3: https://www.rfc-editor.org/rfc/rfc1950
- RFC 1951 - Deflate Compressed Data Format Specification version 1.3: https://www.rfc-editor.org/rfc/rfc1951
参考にした情報源リンク
- Go CL 13171046: compress/zlib: add Reset method to Writer.: https://golang.org/cl/13171046
- Go
compress/gzip
パッケージのReset
メソッドの実装 (参考): https://pkg.go.dev/compress/gzip#Writer.Reset - Go
compress/flate
パッケージのReset
メソッドの実装 (参考): https://pkg.go.dev/compress/flate#Writer.Reset - Zlib (Wikipedia): https://ja.wikipedia.org/wiki/Zlib
- Adler-32 (Wikipedia): https://ja.wikipedia.org/wiki/Adler-32