[インデックス 12991] ファイルの概要
このコミットは、Go言語の標準ライブラリである compress/flate パッケージに、エンコーダとデコーダのベンチマークを追加するものです。これにより、flate 圧縮/解凍のパフォーマンス改善の評価が可能になります。
コミット
commit d5b299eda2cc8388bbf8acd9fa03c05a5b76aa2c
Author: Dave Cheney <dave@cheney.net>
Date: Sun Apr 29 20:41:13 2012 +1000
compress/flate: add Encoder/Decoder benchmarks
In CL 6127051, nigeltao suggested that further gains
were possible by improving the performance of flate.
This CL adds a set of benchmarks (based on compress/lzw)
that can be used to judge any future improvements.
R=nigeltao
CC=golang-dev
https://golang.org/cl/6128049
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/d5b299eda2cc8388bbf8acd9fa03c05a5b76aa2c
元コミット内容
compress/flate: add Encoder/Decoder benchmarks
このコミットは、compress/flate パッケージにエンコーダとデコーダのベンチマークを追加します。
CL 6127051において、nigeltao氏がflateのパフォーマンス改善によってさらなる向上が可能であると示唆しました。このコミットは、将来の改善を評価するために使用できる一連のベンチマーク(compress/lzw に基づく)を追加します。
変更の背景
Go言語の compress/flate パッケージは、DEFLATEアルゴリズムの実装を提供しており、これはZlib、gzip、PNGなどの様々な圧縮形式の基盤となっています。パフォーマンスは、特にデータ転送やストレージにおいて、圧縮ライブラリにとって非常に重要な要素です。
このコミットの背景には、以前の変更(CL 6127051)で nigeltao 氏が flate パッケージのさらなるパフォーマンス改善の可能性を指摘したことがあります。パフォーマンスの改善を行うためには、現在の性能を正確に測定し、変更が性能にどのような影響を与えるかを定量的に評価できる仕組みが必要です。このコミットは、そのための基盤として、エンコーダ(圧縮)とデコーダ(解凍)のベンチマークを追加することを目的としています。これにより、将来の最適化作業がデータに基づいたものとなり、実際の性能向上が確認できるようになります。
前提知識の解説
1. DEFLATEアルゴリズムとFlate圧縮
DEFLATEは、可逆データ圧縮アルゴリズムであり、LZ77アルゴリズムとハフマン符号化の組み合わせを使用します。Go言語の compress/flate パッケージは、このDEFLATEアルゴリズムを実装しており、データストリームを圧縮・解凍するための機能を提供します。
- LZ77: 繰り返し出現するバイト列(シーケンス)を、以前に出現した同じシーケンスへの参照(オフセットと長さ)で置き換えることで圧縮します。
- ハフマン符号化: 出現頻度の高いシンボルには短いビット列を、出現頻度の低いシンボルには長いビット列を割り当てることで、データの平均ビット長を短縮します。
flate パッケージは、io.Writer インターフェースと io.Reader インターフェースを実装しており、それぞれ圧縮データの書き込みと読み込みを可能にします。
2. Go言語のベンチマーク
Go言語には、標準でベンチマークテストを記述するためのフレームワークが組み込まれています。testing パッケージの一部として提供され、go test -bench=. コマンドで実行できます。
testing.B: ベンチマーク関数に渡される構造体で、ベンチマークの実行を制御し、結果を報告するためのメソッドを提供します。- ベンチマーク関数の命名規則: ベンチマーク関数は
BenchmarkXxx(*testing.B)という形式で命名する必要があります。 b.N: ベンチマーク関数が実行されるループの回数を示します。Goのテストフレームワークは、安定した測定結果を得るために、このb.Nの値を自動的に調整します。b.StopTimer()/b.StartTimer(): ベンチマークの計測を一時停止/再開します。セットアップ処理など、測定対象ではない処理の時間を計測から除外するために使用されます。b.SetBytes(n int64): 1回の操作で処理されるバイト数を設定します。これにより、ベンチマーク結果が「操作あたりのns」だけでなく、「バイトあたりのns」や「MB/s」といった形式で表示されるようになり、より直感的なパフォーマンス評価が可能になります。runtime.GC(): ガベージコレクションを強制的に実行します。ベンチマークの実行前にこれを呼び出すことで、以前のテストやベンチマークで割り当てられたメモリが、現在のベンチマークの測定に影響を与えないようにすることができます。特に、メモリを大量に消費する操作のベンチマークでは、GCの影響を排除するために使用されることがあります。ioutil.Discard:io.Writerの実装の一つで、書き込まれたデータをすべて破棄します。ベンチマークにおいて、書き込み操作自体のオーバーヘッドを測定したいが、実際にデータを保持する必要がない場合などに使用されます。
3. 圧縮レベル
compress/flate パッケージでは、圧縮レベルを指定できます。これは、圧縮率と圧縮速度のトレードオフを制御します。
BestSpeed: 最速の圧縮速度を提供しますが、圧縮率は低くなります。DefaultCompression: 速度と圧縮率のバランスが取れたデフォルトの設定です。BestCompression: 最高の圧縮率を提供しますが、圧縮速度は遅くなります。
これらの圧縮レベルは、ベンチマークにおいて異なるシナリオでのパフォーマンスを評価するために使用されます。
技術的詳細
このコミットでは、compress/flate パッケージのエンコーダ(圧縮)とデコーダ(解凍)のパフォーマンスを測定するためのベンチマークが追加されています。ベンチマークは、src/pkg/compress/flate/reader_test.go と src/pkg/compress/flate/writer_test.go の2つの新しいファイルに実装されています。
reader_test.go (デコーダのベンチマーク)
benchmarkDecoder 関数がデコーダのベンチマークの共通ロジックを提供します。
- データ準備:
b.StopTimer(): ベンチマークの計測を一時停止します。b.SetBytes(int64(n)): 処理するバイト数nを設定します。../testdata/e.txtからデータを読み込み、buf0に格納します。このファイルは、ベンチマークの入力データとして使用されます。NewWriterを使用して、指定された圧縮レベルでbuf0の内容を圧縮し、compressedバッファに書き込みます。これにより、デコーダのベンチマークに必要な圧縮済みデータが準備されます。runtime.GC(): ガベージコレクションを強制実行し、メモリの状態をクリーンにします。
- ベンチマーク実行:
b.StartTimer(): ベンチマークの計測を再開します。for i := 0; i < b.N; i++:b.N回ループを実行します。- ループ内で、
NewReaderを使用して圧縮済みデータ (buf1) からflate.Readerを作成し、io.Copy(ioutil.Discard, ...)を使ってその内容を読み捨てます。これにより、解凍処理のパフォーマンスが測定されます。
複数の BenchmarkDecoderXxx 関数が定義されており、それぞれ異なる圧縮レベル (BestSpeed, DefaultCompression, BestCompression) とデータサイズ (1K, 10K, 100K バイト) の組み合わせで benchmarkDecoder を呼び出します。
writer_test.go (エンコーダのベンチマーク)
benchmarkEncoder 関数がエンコーダのベンチマークの共通ロジックを提供します。
- データ準備:
b.StopTimer(): ベンチマークの計測を一時停止します。b.SetBytes(int64(n)): 処理するバイト数nを設定します。../testdata/e.txtからデータを読み込み、buf0に格納します。buf1という名前の大きなバイトスライスを作成し、buf0の内容をnバイト分コピーして埋めます。これは、エンコーダの入力データとして使用されます。runtime.GC(): ガベージコレクションを強制実行します。
- ベンチマーク実行:
b.StartTimer(): ベンチマークの計測を再開します。for i := 0; i < b.N; i++:b.N回ループを実行します。- ループ内で、
NewWriter(ioutil.Discard, level)を使用して、指定された圧縮レベルでflate.Writerを作成します。出力先はioutil.Discardなので、圧縮されたデータは破棄されます。 w.Write(buf1):buf1の内容をライターに書き込み、圧縮処理を実行します。w.Close(): ライターを閉じ、すべてのバッファされたデータがフラッシュされることを保証します。
こちらも、複数の BenchmarkEncoderXxx 関数が定義されており、それぞれ異なる圧縮レベルとデータサイズの組み合わせで benchmarkEncoder を呼び出します。
これらのベンチマークは、compress/lzw パッケージのベンチマークを参考に作成されており、一貫性のある測定方法を提供します。
コアとなるコードの変更箇所
このコミットでは、以下の2つの新しいファイルが追加されています。
src/pkg/compress/flate/reader_test.gosrc/pkg/compress/flate/writer_test.go
これらのファイルは、既存のコードの変更ではなく、新しいテストファイルとして追加されています。
src/pkg/compress/flate/reader_test.go
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package flate
import (
"bytes"
"io"
"io/ioutil"
"runtime"
"testing"
)
func benchmarkDecoder(b *testing.B, level, n int) {
b.StopTimer()
b.SetBytes(int64(n))
buf0, err := ioutil.ReadFile("../testdata/e.txt")
if err != nil {
b.Fatal(err)
}
buf0 = buf0[:10000]
compressed := new(bytes.Buffer)
w, err := NewWriter(compressed, level)
if err != nil {
b.Fatal(err)
}
for i := 0; i < n; i += len(buf0) {
io.Copy(w, bytes.NewBuffer(buf0))
}
w.Close()
buf1 := compressed.Bytes()
buf0, compressed, w = nil, nil, nil
runtime.GC()
b.StartTimer()
for i := 0; i < b.N; i++ {
io.Copy(ioutil.Discard, NewReader(bytes.NewBuffer(buf1)))
}
}
func BenchmarkDecoderBestSpeed1K(b *testing.B) {
benchmarkDecoder(b, BestSpeed, 1e4)
}
func BenchmarkDecoderBestSpeed10K(b *testing.B) {
benchmarkDecoder(b, BestSpeed, 1e5)
}
func BenchmarkDecoderBestSpeed100K(b *testing.B) {
benchmarkDecoder(b, BestSpeed, 1e6)
}
func BenchmarkDecoderDefaultCompression1K(b *testing.B) {
benchmarkDecoder(b, DefaultCompression, 1e4)
}
func BenchmarkDecoderDefaultCompression10K(b *testing.B) {
benchmarkDecoder(b, DefaultCompression, 1e5)
}
func BenchmarkDecoderDefaultCompression100K(b *testing.B) {
benchmarkDecoder(b, DefaultCompression, 1e6)
}
func BenchmarkDecoderBestCompression1K(b *testing.B) {
benchmarkDecoder(b, BestCompression, 1e4)
}
func BenchmarkDecoderBestCompression10K(b *testing.B) {
benchmarkDecoder(b, BestCompression, 1e5)
}
func BenchmarkDecoderBestCompression100K(b *testing.B) {
benchmarkDecoder(b, BestCompression, 1e6)
}
src/pkg/compress/flate/writer_test.go
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package flate
import (
"io/ioutil"
"runtime"
"testing"
)
func benchmarkEncoder(b *testing.B, level, n int) {
b.StopTimer()
b.SetBytes(int64(n))
buf0, err := ioutil.ReadFile("../testdata/e.txt")
if err != nil {
b.Fatal(err)
}
buf0 = buf0[:10000]
buf1 := make([]byte, n)
for i := 0; i < n; i += len(buf0) {
copy(buf1[i:], buf0)
}
buf0 = nil
runtime.GC()
b.StartTimer()
for i := 0; i < b.N; i++ {
w, err := NewWriter(ioutil.Discard, level)
if err != nil {
b.Fatal(err)
}
w.Write(buf1)
w.Close()
}
}
func BenchmarkEncoderBestSpeed1K(b *testing.B) {
benchmarkEncoder(b, BestSpeed, 1e4)
}
func BenchmarkEncoderBestSpeed10K(b *testing.B) {
benchmarkEncoder(b, BestSpeed, 1e5)
}
func BenchmarkEncoderBestCompression1K(b *testing.B) {
benchmarkEncoder(b, BestCompression, 1e4)
}
func BenchmarkEncoderBestCompression10K(b *testing.B) {
benchmarkEncoder(b, BestCompression, 1e5)
}
func BenchmarkEncoderBestCompression100K(b *testing.B) {
benchmarkEncoder(b, BestCompression, 1e6)
}
func BenchmarkEncoderDefaultCompression1K(b *testing.B) {
benchmarkEncoder(b, DefaultCompression, 1e4)
}
func BenchmarkEncoderDefaultCompression10K(b *testing.B) {
benchmarkEncoder(b, DefaultCompression, 1e5)
}
func BenchmarkEncoderDefaultCompression100K(b *testing.B) {
benchmarkEncoder(b, DefaultCompression, 1e6)
}
func BenchmarkEncoderBestCompression1K(b *testing.B) {
benchmarkEncoder(b, BestCompression, 1e4)
}
func BenchmarkEncoderBestCompression10K(b *testing.B) {
benchmarkEncoder(b, BestCompression, 1e5)
}
func BenchmarkEncoderBestCompression100K(b *testing.B) {
benchmarkEncoder(b, BestCompression, 1e6)
}
コアとなるコードの解説
reader_test.go の benchmarkDecoder 関数
この関数は、flate デコーダのパフォーマンスを測定するための中心的なロジックを含んでいます。
b.StopTimer(): ベンチマークの計測を一時停止します。これは、ベンチマークの準備段階(データの読み込み、圧縮など)の時間が測定結果に含まれないようにするためです。b.SetBytes(int64(n)): ベンチマークが1回の操作で処理するバイト数nを設定します。これにより、go test -benchの出力で「MB/s」のようなスループットの指標が表示されるようになります。- 入力データの準備:
ioutil.ReadFile("../testdata/e.txt"): テストデータファイルe.txtを読み込みます。このファイルは、Goのテストスイートで一般的に使用されるサンプルデータです。buf0 = buf0[:10000]: 読み込んだデータの最初の10000バイトを使用します。NewWriter(compressed, level):flate.NewWriterを使用して、指定された圧縮レベル (level) でデータを圧縮し、bytes.Buffer(compressed) に書き込みます。これは、デコーダのベンチマークのために、事前に圧縮されたデータを用意するステップです。io.Copy(w, bytes.NewBuffer(buf0)):buf0の内容を繰り返しflate.Writerにコピーし、指定されたバイト数nになるまで圧縮します。w.Close():flate.Writerを閉じ、すべてのバッファされたデータがcompressedにフラッシュされることを保証します。buf1 := compressed.Bytes(): 圧縮されたデータをバイトスライスとして取得します。buf0, compressed, w = nil, nil, nil: 不要になった参照をnilに設定し、メモリを解放します。runtime.GC(): ガベージコレクションを強制的に実行し、ベンチマークの実行中にGCが予期せず発生して測定結果に影響を与えるのを防ぎます。
- デコード処理のベンチマーク:
b.StartTimer(): ベンチマークの計測を再開します。for i := 0; i < b.N; i++:b.N回のループでデコード処理を実行します。b.Nはgo testコマンドによって自動的に調整されます。NewReader(bytes.NewBuffer(buf1)): 圧縮されたデータbuf1から新しいflate.Readerを作成します。io.Copy(ioutil.Discard, ...):flate.Readerからデータを読み込み、ioutil.Discardに書き込みます。ioutil.Discardは書き込まれたデータをすべて破棄するため、実際のディスクI/Oやメモリ割り当てのオーバーヘッドなしに、デコード処理自体の時間を測定できます。
writer_test.go の benchmarkEncoder 関数
この関数は、flate エンコーダのパフォーマンスを測定するための中心的なロジックを含んでいます。
b.StopTimer()/b.SetBytes(int64(n)):benchmarkDecoderと同様に、計測の一時停止と処理バイト数の設定を行います。- 入力データの準備:
ioutil.ReadFile("../testdata/e.txt"): テストデータファイルe.txtを読み込みます。buf0 = buf0[:10000]: 最初の10000バイトを使用します。buf1 := make([]byte, n): 圧縮するデータbuf1を作成します。for i := 0; i < n; i += len(buf0) { copy(buf1[i:], buf0) }:buf0の内容を繰り返しbuf1にコピーし、指定されたバイト数nになるようにデータを準備します。buf0 = nil: 不要になった参照をnilに設定します。runtime.GC(): ガベージコレクションを強制実行します。
- エンコード処理のベンチマーク:
b.StartTimer(): ベンチマークの計測を再開します。for i := 0; i < b.N; i++:b.N回のループでエンコード処理を実行します。w, err := NewWriter(ioutil.Discard, level):flate.NewWriterを使用して、指定された圧縮レベル (level) でデータを圧縮し、ioutil.Discardに書き込みます。これにより、エンコード処理自体の時間を測定できます。w.Write(buf1): 準備されたデータbuf1をflate.Writerに書き込み、圧縮処理を実行します。w.Close():flate.Writerを閉じ、すべてのバッファされたデータがフラッシュされることを保証します。
これらのベンチマーク関数は、それぞれ異なる圧縮レベル (BestSpeed, DefaultCompression, BestCompression) とデータサイズ (1K, 10K, 100K バイト) の組み合わせで呼び出されるように、複数の BenchmarkXxx 関数が定義されています。これにより、様々なシナリオでの flate パッケージのパフォーマンス特性を詳細に分析することが可能になります。
関連リンク
- Go言語の
compress/flateパッケージのドキュメント: https://pkg.go.dev/compress/flate - Go言語の
testingパッケージのドキュメント (ベンチマークについて): https://pkg.go.dev/testing - DEFLATEアルゴリズムに関するWikipedia記事: https://ja.wikipedia.org/wiki/DEFLATE
参考にした情報源リンク
- Go言語のコミット履歴 (GitHub): https://github.com/golang/go/commits/master
- Go Code Review Comments (ベンチマークの書き方に関するガイドライン): https://go.dev/doc/effective_go#benchmarking
- CL 6127051 (nigeltao氏によるflateパフォーマンス改善の示唆): このコミットメッセージに記載されているCL番号ですが、直接的なリンクは提供されていません。Goのコードレビューシステム (Gerrit) の古いCL番号は、現在のGitHubのコミットハッシュとは直接対応しない場合があります。
- Go言語の
compress/lzwパッケージのベンチマーク (参考元): https://pkg.go.dev/compress/lzw (具体的なベンチマークコードはソースコード内で確認できます)