Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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/gzipcompress/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 メソッドは以下の処理を行います。

  1. 出力先 io.Writer の更新: z.w = w により、zlib.Writer がデータを書き込む先の io.Writer を新しいものに設定します。
  2. 圧縮レベルと辞書の維持: z.levelz.dict は変更されません。これは、Reset が同じ圧縮設定で再利用されることを意図しているためです。
  3. 内部コンプレッサーのリセット: z.compressor != nil の場合、つまり内部の flate.Writer が既に存在する場合、z.compressor.Reset(w) を呼び出して flate.Writer の状態をリセットし、新しい出力先 w に接続します。これにより、Deflate圧縮器も再利用されます。
  4. Adler-32ダイジェストのリセット: z.digest != nil の場合、つまりAdler-32チェックサム計算器が既に存在する場合、z.digest.Reset() を呼び出してチェックサム計算器の状態を初期化します。これにより、新しいデータのチェックサムをゼロから計算できるようになります。
  5. エラー状態のクリア: z.err = nil により、以前の圧縮操作で発生したエラー状態をクリアします。
  6. スクラッチバッファのクリア: z.scratch = [4]byte{} により、内部で使用される一時的なバッファをクリアします。
  7. ヘッダー書き込みフラグのリセット: 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 メソッドの動作を検証するための新しいテスト関数 testFileLevelDictResetTestWriterReset が追加されています。これらのテストは、様々な圧縮レベルと辞書設定でデータを圧縮し、その後 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.levelz.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.Writeradler32.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.compressornil の場合にのみ、新しい flate.Writeradler32.Hash インスタンスが作成されるようになりました。
  • Reset メソッドが呼び出された場合、z.compressor は既に存在し、z.compressor.Reset(w) によってリセットされています。したがって、この if ブロック内のコードは実行されず、既存の flate.Writeradler32.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 に向けて同じデータを再度圧縮します。最後に、初回圧縮の結果 outReset 後の圧縮結果 out2 が完全に一致することを確認します。これにより、Reset メソッドが Writer の状態を正しく初期化し、一貫した圧縮出力を生成できることが保証されます。

TestWriterReset 関数は、この testFileLevelDictReset を様々なファイル、圧縮レベル、および辞書設定で繰り返し呼び出し、広範なテストカバレッジを提供します。

関連リンク

参考にした情報源リンク