[インデックス 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つの主要なボトルネックがありました。
- flate圧縮: ZIPファイルにデータを書き込む際、通常はデータを圧縮(flate圧縮)します。4GBものデータを圧縮する処理は非常にCPUと時間を消費します。
- 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
オプションを指定してテストを実行していることを意味します。開発者はこのフラグを利用して、時間のかかるテスト(例:ネットワークアクセス、大規模なファイル操作、計算量の多い処理を伴うテスト)をスキップし、テストスイート全体の実行時間を短縮することができます。
このコミットの変更前は、TestZip64
は testing.Short()
をチェックしていましたが、それでも go test
を通常実行した場合には非常に時間がかかっていました。
技術的詳細
このコミットの核心は、TestZip64
の実行時間を短縮するために、実際のデータ圧縮とCRC32計算をバイパスする新しいメカニズムを導入した点にあります。これは、テストの目的が「Zip64形式が正しく扱えるか」であり、「圧縮やCRC32計算のパフォーマンス」ではないため、このような最適化が可能です。
具体的には、以下の2つの主要な変更が導入されました。
-
rleBuffer
の導入:rleBuffer
は "run-length-encoded byte buffer" の略で、連続する同じバイトを効率的に格納するカスタムのio.Writer
およびio.ReaderAt
実装です。- 従来の
TestZip64
では、bytes.Buffer
を使用して4GBのデータをメモリ上に構築していました。このデータは、ほとんどが同じ文字(.
)の繰り返しで構成されていました。 rleBuffer
は、この繰り返しパターンを利用して、実際の4GBのデータをメモリ上に保持することなく、論理的に4GBのデータが存在するかのように振る舞います。これにより、メモリ使用量を大幅に削減し、データのコピーや操作にかかる時間を短縮します。Write
メソッドは、書き込まれるバイトが直前のバイトと同じであれば、既存のrepeatedByte
エントリの長さを増やすだけで済みます。異なるバイトが来たら新しいエントリを作成します。ReadAt
メソッドは、指定されたオフセットからデータを読み出す際に、rleBuffer
内のrepeatedByte
エントリを検索し、そこから必要なバイトを生成して返します。これにより、ランダムアクセス読み取りも効率的に行えます。
-
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
ファイルが変更されています。
主な変更点は以下の通りです。
rleBuffer
構造体とそのメソッド (Size
,Write
,ReadAt
) の追加。TestRLEBuffer
関数の追加。 (rleBuffer
自体のテスト)fakeHash32
構造体とそのメソッド (Write
,Sum32
) の追加。TestZip64
関数のリファクタリングとtestZip64
ヘルパー関数の導入。TestZip64
はtestZip64(t, 1<<32)
を呼び出すだけになる。testZip64
内で、bytes.Buffer
の代わりにrleBuffer
を使用。w.Create
の代わりにw.CreateHeader
を使用し、Method: Store
(無圧縮) を指定。f.(*fileWriter).crc32 = fakeHash32{}
を追加し、CRC32計算を無効化。- 読み込み時も
rc.(*checksumReader).hash = fakeHash32{}
を追加し、CRC32チェックを無効化。
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
は、特定のバイト b
が n
回繰り返され、その開始オフセットが 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 }
fakeHash32
は hash.Hash32
インターフェースを満たすためのダミー実装です。
Write
メソッドは、書き込まれたデータを何もせずに破棄し、書き込まれたバイト数だけを返します。
Sum32
メソッドは、常に 0
を返します。
これにより、archive/zip
パッケージが内部でCRC32計算を行う際に、このダミー実装が使われることで、実際の計算がスキップされます。
testZip64
と TestZip64
の変更
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)
}
}
// ... (残りの検証ロジックは上記コミット内容を参照)
}
TestZip64
は testing.Short()
をチェックし、フルテストが必要な場合に testZip64
を呼び出します。
testZip64
関数は、bytes.Buffer
の代わりに新しく導入された rleBuffer
を使用することで、4GBのデータを効率的に扱います。
w.CreateHeader
を使用し、Method: Store
を指定することで、データが圧縮されずにそのまま格納されるようにします。これにより、flate圧縮のオーバーヘッドがなくなります。
f.(*fileWriter).crc32 = fakeHash32{}
と rc.(*checksumReader).hash = fakeHash32{}
を設定することで、書き込み時と読み込み時の両方でCRC32計算とチェックを無効化します。
これらの変更により、テストはZip64のメタデータ処理に焦点を当て、実際のデータ操作によるパフォーマンスボトルネックを回避しています。
関連リンク
- Go言語
archive/zip
パッケージのドキュメント: https://pkg.go.dev/archive/zip - ZIPファイル形式の仕様 (PKWARE): https://pkware.com/docs/casestudies/APPNOTE.TXT
- Go言語のテストに関するドキュメント: https://go.dev/doc/code#testing
参考にした情報源リンク
- Go言語のコミット履歴 (GitHub): https://github.com/golang/go/commits/master
- Go言語のコードレビューシステム (Gerrit): https://go.dev/cl/12950043 (コミットメッセージに記載されているCLリンク)
- CRC32に関する情報: https://ja.wikipedia.org/wiki/CRC
- Deflate圧縮アルゴリズムに関する情報: https://ja.wikipedia.org/wiki/Deflate
- Zip64に関する情報: https://en.wikipedia.org/wiki/Zip_(file_format)#ZIP64
- Go言語の
testing
パッケージ: https://pkg.go.dev/testing - Go言語の
hash/crc32
パッケージ: https://pkg.go.dev/hash/crc32 - Go言語の
compress/flate
パッケージ: https://pkg.go.dev/compress/flate - Go言語の
io
パッケージ: https://pkg.go.dev/io - Go言語の
bytes
パッケージ: https://pkg.go.dev/bytes