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

[インデックス 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.WriterReset メソッドを追加することで、既存のライターを再利用し、新しい出力ストリームに接続できるようにすることが提案されていました。これにより、オブジェクトの再利用が可能になり、アロケーションを削減し、パフォーマンスを向上させることが期待されます。

また、このコミットは 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.Writerio.Writer インターフェースを実装し、データをDEFLATE形式で圧縮します。

hash/crc32 パッケージ

hash/crc32 パッケージは、CRC-32チェックサムアルゴリズムを実装しています。GZIP形式では、圧縮されていない元のデータのCRC-32チェックサムがヘッダーに格納され、データの整合性検証に使用されます。crc32.NewIEEE() は、IEEEポリノミアルに基づくCRC-32ハッシュを計算する hash.Hash32 インターフェースを返す関数です。

Reset メソッドの概念

Go言語の標準ライブラリでは、bufio.Readerbufio.Writer など、一部のI/O関連の型に Reset メソッドが提供されています。このメソッドは、既存のインスタンスの内部状態をリセットし、新しい基盤となるI/Oストリーム(io.Readerio.Writer)に接続し直すことを可能にします。これにより、新しいインスタンスをアロケートする代わりに、既存のインスタンスを再利用できるため、特にループ内で多数のI/O操作を行う場合に、ガベージコレクションの負荷を軽減し、パフォーマンスを向上させることができます。

技術的詳細

このコミットの主要な目的は、gzip.Writer の再利用を可能にする Reset メソッドを導入することです。これを実現するために、以下の変更が行われています。

  1. Writer 構造体の変更:

    • wroteHeader bool フィールドが追加されました。これは、GZIPヘッダーが既に書き込まれたかどうかを追跡するためのフラグです。GZIPヘッダーはストリームの開始時に一度だけ書き込まれる必要があります。Reset 後に新しいストリームを開始する際には、このフラグがリセットされる必要があります。
    • 既存のフィールドの順序が変更されていますが、これは機能的な変更ではなく、構造体のアライメントやメモリレイアウトの最適化に関連する可能性があります。
  2. init ヘルパー関数の導入:

    • *Writer.init(w io.Writer, level int) という新しいプライベートヘルパー関数が導入されました。この関数は、Writer インスタンスの初期化ロジックをカプセル化します。
    • NewWriterLevelReset の両方から呼び出され、重複コードを排除し、初期化の一貫性を保証します。
    • init 関数内では、既存の digest (CRC32ハッシュ) と compressor (flate.Writer) が存在する場合、それらの Reset メソッドを呼び出して状態をリセットし、再利用を試みます。存在しない場合は新しく作成します。これにより、内部オブジェクトも可能な限り再利用されます。
  3. Writer.Reset メソッドの実装:

    • func (z *Writer) Reset(w io.Writer) メソッドが追加されました。
    • このメソッドは、z.init(w, z.level) を呼び出すことで、Writer の内部状態をリセットし、新しい io.Writer w に接続します。z.level は元の圧縮レベルを保持します。
    • これにより、Writer インスタンスを破棄して再作成する代わりに、既存のインスタンスを新しい出力ストリームに対して再利用できるようになります。
  4. Write メソッドの変更:

    • GZIPヘッダーの遅延書き込みロジックが z.compressor == nil から !z.wroteHeader に変更されました。これは、Reset 後に compressor が再利用される可能性があるため、ヘッダー書き込みのトリガーを wroteHeader フラグに依存させるように修正されたものです。
    • z.compressornil の場合にのみ flate.NewWriter を呼び出すように変更され、Reset によって compressor が再利用される場合に新しい flate.Writer が不必要に作成されるのを防ぎます。
  5. Flush および Close メソッドの変更:

    • これらのメソッドでも、GZIPヘッダーの遅延書き込みの条件が z.compressor == nil から !z.wroteHeader に変更されました。これにより、Reset 後もヘッダーが正しく書き込まれることが保証されます。
  6. テストケースの追加:

    • 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) が再利用される可能性があるため、compressornil でない場合でもヘッダーが書き込まれていない状況が発生し得ます。この 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 のすべてのフィールドを新しいゼロ値または指定された値で上書きし、効果的に状態をリセットします。これにより、closederr などの以前の状態がクリアされます。

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 に変更されました。これにより、FlushClose が呼び出された時点でまだヘッダーが書き込まれていない場合(例えば、データが全く書き込まれていない場合)、ヘッダーが正しく書き込まれることが保証されます。また、Flush メソッドでは、ヘッダー書き込み中にエラーが発生した場合にそのエラーを返すように修正されています。

関連リンク

参考にした情報源リンク

  • Go issue #6138: compress/gzip: add Reset method to Writer (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)