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

[インデックス 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.gosrc/pkg/compress/flate/writer_test.go の2つの新しいファイルに実装されています。

reader_test.go (デコーダのベンチマーク)

benchmarkDecoder 関数がデコーダのベンチマークの共通ロジックを提供します。

  1. データ準備:
    • b.StopTimer(): ベンチマークの計測を一時停止します。
    • b.SetBytes(int64(n)): 処理するバイト数 n を設定します。
    • ../testdata/e.txt からデータを読み込み、buf0 に格納します。このファイルは、ベンチマークの入力データとして使用されます。
    • NewWriter を使用して、指定された圧縮レベルで buf0 の内容を圧縮し、compressed バッファに書き込みます。これにより、デコーダのベンチマークに必要な圧縮済みデータが準備されます。
    • runtime.GC(): ガベージコレクションを強制実行し、メモリの状態をクリーンにします。
  2. ベンチマーク実行:
    • 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 関数がエンコーダのベンチマークの共通ロジックを提供します。

  1. データ準備:
    • b.StopTimer(): ベンチマークの計測を一時停止します。
    • b.SetBytes(int64(n)): 処理するバイト数 n を設定します。
    • ../testdata/e.txt からデータを読み込み、buf0 に格納します。
    • buf1 という名前の大きなバイトスライスを作成し、buf0 の内容を n バイト分コピーして埋めます。これは、エンコーダの入力データとして使用されます。
    • runtime.GC(): ガベージコレクションを強制実行します。
  2. ベンチマーク実行:
    • 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つの新しいファイルが追加されています。

  1. src/pkg/compress/flate/reader_test.go
  2. 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.gobenchmarkDecoder 関数

この関数は、flate デコーダのパフォーマンスを測定するための中心的なロジックを含んでいます。

  1. b.StopTimer(): ベンチマークの計測を一時停止します。これは、ベンチマークの準備段階(データの読み込み、圧縮など)の時間が測定結果に含まれないようにするためです。
  2. b.SetBytes(int64(n)): ベンチマークが1回の操作で処理するバイト数 n を設定します。これにより、go test -bench の出力で「MB/s」のようなスループットの指標が表示されるようになります。
  3. 入力データの準備:
    • 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が予期せず発生して測定結果に影響を与えるのを防ぎます。
  4. デコード処理のベンチマーク:
    • b.StartTimer(): ベンチマークの計測を再開します。
    • for i := 0; i < b.N; i++: b.N 回のループでデコード処理を実行します。b.Ngo test コマンドによって自動的に調整されます。
    • NewReader(bytes.NewBuffer(buf1)): 圧縮されたデータ buf1 から新しい flate.Reader を作成します。
    • io.Copy(ioutil.Discard, ...): flate.Reader からデータを読み込み、ioutil.Discard に書き込みます。ioutil.Discard は書き込まれたデータをすべて破棄するため、実際のディスクI/Oやメモリ割り当てのオーバーヘッドなしに、デコード処理自体の時間を測定できます。

writer_test.gobenchmarkEncoder 関数

この関数は、flate エンコーダのパフォーマンスを測定するための中心的なロジックを含んでいます。

  1. b.StopTimer() / b.SetBytes(int64(n)): benchmarkDecoder と同様に、計測の一時停止と処理バイト数の設定を行います。
  2. 入力データの準備:
    • 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(): ガベージコレクションを強制実行します。
  3. エンコード処理のベンチマーク:
    • 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): 準備されたデータ buf1flate.Writer に書き込み、圧縮処理を実行します。
    • w.Close(): flate.Writer を閉じ、すべてのバッファされたデータがフラッシュされることを保証します。

これらのベンチマーク関数は、それぞれ異なる圧縮レベル (BestSpeed, DefaultCompression, BestCompression) とデータサイズ (1K, 10K, 100K バイト) の組み合わせで呼び出されるように、複数の BenchmarkXxx 関数が定義されています。これにより、様々なシナリオでの flate パッケージのパフォーマンス特性を詳細に分析することが可能になります。

関連リンク

参考にした情報源リンク

  • 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 (具体的なベンチマークコードはソースコード内で確認できます)