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

[インデックス 17262] ファイルの概要

このコミットは、Go言語の標準ライブラリ archive/zip パッケージ内の Zip64 テストの実行速度を改善することを目的としています。具体的には、テストが4GBものデータを扱う際に発生していた、時間のかかる圧縮(flate)とCRC32チェックの処理を回避することで、テスト時間を大幅に短縮しています。

コミット

archive/zip: Zip64 テストの高速化

以前は76秒ほどかかっていた。4GBのデータに対するflateとcrc32を回避することで、現在はわずか12秒になった。まだ遅いテストではあるが、-short を忘れて実行しても苦痛ではなくなった。

R=golang-dev, adg CC=golang-dev https://golang.org/cl/12950043

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/ec837ad73c20d9b33d8aea9d79ce68bf95598544

元コミット内容

commit ec837ad73c20d9b33d8aea9d79ce68bf95598544
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Wed Aug 14 23:21:57 2013 -0700

    archive/zip: speed up Zip64 test
    
    Took 76 seconds or so before. By avoiding flate and crc32 on
    4GB of data, it's now only 12 seconds.  Still a slow test, but
    not painful to run anymore when you forget -short.
    
    R=golang-dev, adg
    CC=golang-dev
    https://golang.org/cl/12950043

変更の背景

Go言語の archive/zip パッケージには、Zip64形式を扱うためのテスト TestZip64 が存在します。Zip64は、従来のZIP形式のファイルサイズやエントリ数に関する4GBの制限を超えるファイルを扱うための拡張仕様です。この TestZip64 は、実際に4GB(正確には2^32バイト)もの巨大なデータを生成し、それをZIPファイルとして書き込み、その後読み戻すという処理を行っていました。

このテストの実行には、以下の2つの主要なボトルネックがありました。

  1. flate圧縮: ZIPファイルにデータを書き込む際、通常はデータを圧縮(flate圧縮)します。4GBものデータを圧縮する処理は非常にCPUと時間を消費します。
  2. CRC32チェック: ZIPファイルのエントリには、データの整合性を保証するためのCRC32チェックサムが含まれます。4GBのデータに対してCRC32を計算する処理もまた、時間がかかります。

これらの処理が原因で、TestZip64 の実行には約76秒もの時間がかかっていました。これは開発者がテストを実行する際に大きな負担となり、特に go test -short オプションを付け忘れた場合に、テストスイート全体の実行時間を著しく長くしていました。このコミットの目的は、テストの正確性を損なうことなく、この実行時間を許容できるレベルまで短縮することでした。

前提知識の解説

ZIPファイル形式とZip64

ZIPファイルは、複数のファイルを1つのアーカイブにまとめるための一般的なファイル形式です。各ファイルは「エントリ」としてアーカイブ内に格納され、通常は圧縮されて保存されます。

Zip64 は、従来のZIP形式が持ついくつかの制限を克服するために導入された拡張仕様です。主な制限は以下の通りです。

  • ファイルサイズ: 圧縮前および圧縮後のファイルサイズが4GB(2^32バイト)を超えるファイルを扱えない。
  • エントリ数: アーカイブ内のエントリ数が65,535個を超えるファイルを扱えない。
  • アーカイブサイズ: ZIPアーカイブ自体のサイズが4GBを超えるファイルを扱えない。

Zip64は、これらの制限を解除するために、ファイルサイズやオフセットを格納するフィールドを32ビットから64ビットに拡張します。これにより、非常に大きなファイルや多数のファイルを単一のZIPアーカイブに格納することが可能になります。

CRC32 (Cyclic Redundancy Check)

CRC32は、データの整合性をチェックするために広く用いられるエラー検出コードの一種です。ZIPファイルでは、各エントリの圧縮前データに対してCRC32値が計算され、ファイルヘッダに格納されます。これにより、ファイルが破損していないか、転送中に改ざんされていないかを確認できます。

flate圧縮

flateは、Deflateアルゴリズムに基づくデータ圧縮形式です。ZIPファイルでは、通常、ファイルデータはflate圧縮されて保存されます。これにより、アーカイブのサイズを小さくし、ディスクスペースを節約したり、ネットワーク転送時間を短縮したりできます。

Go言語のテストと testing.Short()

Go言語には、標準でテストフレームワークが組み込まれています。go test コマンドでテストを実行できます。

testing.Short() は、テスト関数内で呼び出すことができる関数です。この関数が true を返す場合、それはユーザーが go test -short オプションを指定してテストを実行していることを意味します。開発者はこのフラグを利用して、時間のかかるテスト(例:ネットワークアクセス、大規模なファイル操作、計算量の多い処理を伴うテスト)をスキップし、テストスイート全体の実行時間を短縮することができます。

このコミットの変更前は、TestZip64testing.Short() をチェックしていましたが、それでも go test を通常実行した場合には非常に時間がかかっていました。

技術的詳細

このコミットの核心は、TestZip64 の実行時間を短縮するために、実際のデータ圧縮とCRC32計算をバイパスする新しいメカニズムを導入した点にあります。これは、テストの目的が「Zip64形式が正しく扱えるか」であり、「圧縮やCRC32計算のパフォーマンス」ではないため、このような最適化が可能です。

具体的には、以下の2つの主要な変更が導入されました。

  1. rleBuffer の導入:

    • rleBuffer は "run-length-encoded byte buffer" の略で、連続する同じバイトを効率的に格納するカスタムの io.Writer および io.ReaderAt 実装です。
    • 従来の TestZip64 では、bytes.Buffer を使用して4GBのデータをメモリ上に構築していました。このデータは、ほとんどが同じ文字(.)の繰り返しで構成されていました。
    • rleBuffer は、この繰り返しパターンを利用して、実際の4GBのデータをメモリ上に保持することなく、論理的に4GBのデータが存在するかのように振る舞います。これにより、メモリ使用量を大幅に削減し、データのコピーや操作にかかる時間を短縮します。
    • Write メソッドは、書き込まれるバイトが直前のバイトと同じであれば、既存の repeatedByte エントリの長さを増やすだけで済みます。異なるバイトが来たら新しいエントリを作成します。
    • ReadAt メソッドは、指定されたオフセットからデータを読み出す際に、rleBuffer 内の repeatedByte エントリを検索し、そこから必要なバイトを生成して返します。これにより、ランダムアクセス読み取りも効率的に行えます。
  2. fakeHash32 の導入:

    • fakeHash32 は、hash.Hash32 インターフェースを実装するダミーの型です。
    • この型は、Write メソッドが受け取ったデータを単に破棄し、Sum32 メソッドが常に 0 を返します。
    • TestZip64 内で、zip.Writer が内部的に使用するCRC32ハッシュ計算器を fakeHash32 のインスタンスに置き換えることで、実際のCRC32計算をスキップします。
    • これにより、4GBのデータに対するCRC32計算のオーバーヘッドが完全に排除されます。

これらの変更により、TestZip64 は、データの内容を実際に圧縮したり、その整合性をチェックしたりすることなく、Zip64形式のヘッダや構造が正しく処理されることを検証できるようになりました。テストの目的が「Zip64のメタデータ処理」であるため、この最適化はテストの有効性を損ないません。

また、TestZip64 関数自体が testZip64 というヘルパー関数にリファクタリングされ、BenchmarkZip64Test という新しいベンチマークテストも追加されました。このベンチマークは、testZip64 をより小さなデータサイズ(1<<26バイト、つまり64MB)で実行し、テストのパフォーマンスを測定するために使用されます。

コアとなるコードの変更箇所

src/pkg/archive/zip/zip_test.go ファイルが変更されています。

主な変更点は以下の通りです。

  1. rleBuffer 構造体とそのメソッド (Size, Write, ReadAt) の追加。
  2. TestRLEBuffer 関数の追加。 (rleBuffer 自体のテスト)
  3. fakeHash32 構造体とそのメソッド (Write, Sum32) の追加。
  4. TestZip64 関数のリファクタリングと testZip64 ヘルパー関数の導入。
    • TestZip64testZip64(t, 1<<32) を呼び出すだけになる。
    • testZip64 内で、bytes.Buffer の代わりに rleBuffer を使用。
    • w.Create の代わりに w.CreateHeader を使用し、Method: Store (無圧縮) を指定。
    • f.(*fileWriter).crc32 = fakeHash32{} を追加し、CRC32計算を無効化。
    • 読み込み時も rc.(*checksumReader).hash = fakeHash32{} を追加し、CRC32チェックを無効化。
  5. BenchmarkZip64Test 関数の追加。

コアとなるコードの解説

rleBuffer

type repeatedByte struct {
	off int64
	b   byte
	n   int64
}

// rleBuffer is a run-length-encoded byte buffer.
// It's an io.Writer (like a bytes.Buffer) and also an io.ReaderAt,
// allowing random-access reads.
type rleBuffer struct {
	buf []repeatedByte
}

func (r *rleBuffer) Size() int64 {
	if len(r.buf) == 0 {
		return 0
	}
	last := &r.buf[len(r.buf)-1]
	return last.off + last.n
}

func (r *rleBuffer) Write(p []byte) (n int, err) {
	// ... (実装は上記コミット内容を参照)
}

func (r *rleBuffer) ReadAt(p []byte, off int64) (n int, err) {
	// ... (実装は上記コミット内容を参照)
}

rleBuffer は、repeatedByte のスライス buf を内部に持ちます。repeatedByte は、特定のバイト bn 回繰り返され、その開始オフセットが off であることを記録します。 Write メソッドは、入力バイトスライス p を受け取り、連続する同じバイトを repeatedByte エントリとして効率的に記録します。 ReadAt メソッドは、指定されたオフセット off から p の長さだけデータを読み出します。内部の repeatedByte エントリを検索し、必要なバイトを「生成」して p に書き込みます。これにより、実際の4GBのデータがメモリに存在しなくても、あたかも存在するかのように振る舞います。

fakeHash32

// fakeHash32 is a dummy Hash32 that always returns 0.
type fakeHash32 struct {
	hash.Hash32
}

func (fakeHash32) Write(p []byte) (int, error) { return len(p), nil }
func (fakeHash32) Sum32() uint32               { return 0 }

fakeHash32hash.Hash32 インターフェースを満たすためのダミー実装です。 Write メソッドは、書き込まれたデータを何もせずに破棄し、書き込まれたバイト数だけを返します。 Sum32 メソッドは、常に 0 を返します。 これにより、archive/zip パッケージが内部でCRC32計算を行う際に、このダミー実装が使われることで、実際の計算がスキップされます。

testZip64TestZip64 の変更

func TestZip64(t *testing.T) {
	if testing.Short() {
		t.Skip("slow test; skipping")
	}
	const size = 1 << 32 // before the "END\n" part
	testZip64(t, size)
}

func testZip64(t testing.TB, size int64) {
	const chunkSize = 1024
	chunks := int(size / chunkSize)
	// write 2^32 bytes plus "END\n" to a zip file
	buf := new(rleBuffer) // bytes.Buffer から rleBuffer に変更
	w := NewWriter(buf)
	f, err := w.CreateHeader(&FileHeader{ // Create から CreateHeader に変更
		Name:   "huge.txt",
		Method: Store, // 圧縮メソッドを Store (無圧縮) に指定
	})
	if err != nil {
		t.Fatal(err)
	}
	f.(*fileWriter).crc32 = fakeHash32{} // CRC32計算を無効化
	chunk := make([]byte, chunkSize)
	for i := range chunk {
		chunk[i] = '.'
	}
	for i := 0; i < chunks; i++ {
		_, err := f.Write(chunk)
		if err != nil {
			t.Fatal("write chunk:", err)
		}
	}
	end := []byte("END\n")
	_, err = f.Write(end)
	if err != nil {
		t.Fatal("write end:", err)
	}
	if err := w.Close(); err != nil {
		t.Fatal("close:", err)
	}

	// read back zip file and check that we get to the end of it
	r, err := NewReader(buf, int64(buf.Size())) // bytes.NewReader(buf.Bytes()) から buf に変更
	if err != nil {
		t.Fatal("reader:", err)
	}
	f0 := r.File[0]
	rc, err := f0.Open()
	if err != nil {
		t.Fatal("opening:", err)
	}
	rc.(*checksumReader).hash = fakeHash32{} // 読み込み時のCRC32チェックを無効化
	for i := 0; i < chunks; i++ {
		_, err := io.ReadFull(rc, chunk)
		if err != nil {
			t.Fatal("read:", err)
		}
	}
	// ... (残りの検証ロジックは上記コミット内容を参照)
}

TestZip64testing.Short() をチェックし、フルテストが必要な場合に testZip64 を呼び出します。 testZip64 関数は、bytes.Buffer の代わりに新しく導入された rleBuffer を使用することで、4GBのデータを効率的に扱います。 w.CreateHeader を使用し、Method: Store を指定することで、データが圧縮されずにそのまま格納されるようにします。これにより、flate圧縮のオーバーヘッドがなくなります。 f.(*fileWriter).crc32 = fakeHash32{}rc.(*checksumReader).hash = fakeHash32{} を設定することで、書き込み時と読み込み時の両方でCRC32計算とチェックを無効化します。

これらの変更により、テストはZip64のメタデータ処理に焦点を当て、実際のデータ操作によるパフォーマンスボトルネックを回避しています。

関連リンク

参考にした情報源リンク