[インデックス 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.go
src/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 (具体的なベンチマークコードはソースコード内で確認できます)