[インデックス 18343] ファイルの概要
このコミットは、Go言語の標準ライブラリであるbufio
パッケージ内のベンチマークの振る舞いを修正するものです。具体的には、ベンチマークがtesting
パッケージに対して不正確な情報を提供していた問題を解決し、より正確なパフォーマンス測定を可能にしています。
コミット
commit 0ad2cd004c94c664dfee3ff16e9d587467f99883
Author: Dmitriy Vyukov <dvyukov@google.com>
Date: Thu Jan 23 15:13:21 2014 -0500
bufio: fix benchmarks behavior
Currently the benchmarks lie to testing package by doing O(N)
work under StopTimer. And that hidden O(N) actually consitutes
the bulk of benchmark work (e.g includes GC per iteration).
This behavior accounts for windows-amd64-race builder hangs.
Before:
BenchmarkReaderCopyOptimal-4 1000000 1861 ns/op
BenchmarkReaderCopyUnoptimal-4 500000 3327 ns/op
BenchmarkReaderCopyNoWriteTo-4 50000 34549 ns/op
BenchmarkWriterCopyOptimal-4 100000 16849 ns/op
BenchmarkWriterCopyUnoptimal-4 500000 3126 ns/op
BenchmarkWriterCopyNoReadFrom-4 50000 34609 ns/op
ok bufio 65.273s
After:
BenchmarkReaderCopyOptimal-4 10000000 172 ns/op
BenchmarkReaderCopyUnoptimal-4 10000000 267 ns/op
BenchmarkReaderCopyNoWriteTo-4 100000 22905 ns/op
BenchmarkWriterCopyOptimal-4 10000000 170 ns/op
BenchmarkWriterCopyUnoptimal-4 10000000 226 ns/op
BenchmarkWriterCopyNoReadFrom-4 100000 20575 ns/op
ok bufio 14.074s
Note the change in total time.
LGTM=alex.brainman, rsc
R=golang-codereviews, alex.brainman, rsc
CC=golang-codereviews
https://golang.org/cl/51360046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/0ad2cd004c94c664dfee3ff16e9d587467f99883
元コミット内容
このコミットの元の内容は、bufio
パッケージのベンチマークがtesting
パッケージに対して不正確な測定結果を報告していたというものです。具体的には、ベンチマークの各イテレーションにおいて、b.StopTimer()
とb.StartTimer()
の間にO(N)
の作業(例えば、バッファの初期化やGCの発生など)が含まれており、これがベンチマークの真の実行時間を歪めていました。この不正確な測定は、特にwindows-amd64-race
ビルダのハングアップを引き起こす原因となっていました。
コミットメッセージには、修正前と修正後のベンチマーク結果が示されており、修正後にはns/op
(1操作あたりのナノ秒)の値が大幅に改善され、全体の実行時間も短縮されていることが強調されています。
変更の背景
Go言語のベンチマークは、testing
パッケージが提供するBenchmark
関数とtesting.B
型を使用して記述されます。testing.B
には、ベンチマーク対象のコードの実行時間を正確に測定するためのStartTimer()
とStopTimer()
メソッドがあります。これらのメソッドは、ベンチマークのセットアップやクリーンアップなど、測定対象外の処理を時間計測から除外するために使用されます。
しかし、このコミットが修正しようとしている問題は、ベンチマークコードがStopTimer()
とStartTimer()
の間に、実際には測定対象となるべき、あるいは測定結果に影響を与えるような処理(O(N)
の作業)を含んでいたことです。この「隠れた作業」には、各イテレーションでのメモリ割り当てやガベージコレクション(GC)が含まれていました。
このような不正確なベンチマークは、以下のような問題を引き起こします。
- 誤解を招くパフォーマンスデータ: 開発者はベンチマーク結果を見て、実際のパフォーマンスよりも良い、あるいは悪いと誤解する可能性があります。これにより、最適化の優先順位付けや設計判断が誤る可能性があります。
- CI/CDパイプラインの不安定化:
windows-amd64-race
ビルダのハングアップという具体的な問題が示されているように、ベンチマークの実行時間が予期せず長くなったり、リソースを過剰に消費したりすることで、継続的インテグレーション/デリバリー(CI/CD)パイプラインが不安定になることがあります。特にrace
検出器が有効な環境では、メモリ割り当てやGCのオーバーヘッドが顕著になり、問題が表面化しやすくなります。 - 最適化の妨げ: 真のボトルネックが隠蔽されるため、開発者がパフォーマンス改善のための適切な箇所を特定しにくくなります。
このコミットは、これらの問題を解決し、bufio
パッケージのベンチマークがより信頼性の高いパフォーマンスデータを提供するようにすることを目的としています。
前提知識の解説
Go言語のベンチマーク
Go言語では、go test
コマンドを使用してユニットテストだけでなくベンチマークも実行できます。ベンチマーク関数はBenchmarkXxx(*testing.B)
というシグネチャを持ち、testing.B
型の引数を受け取ります。
b.N
: ベンチマーク関数内のループで実行されるイテレーション回数を示します。testing
パッケージが自動的に調整し、統計的に有意な結果が得られるようにします。b.ResetTimer()
: タイマーをリセットします。通常、ベンチマークループの直前に呼び出され、セットアップコードの時間を測定から除外します。b.StartTimer()
/b.StopTimer()
: これらのメソッドは、ベンチマークループ内で特定のコードブロックの時間を測定対象に含めたり、除外したりするために使用されます。StopTimer()
が呼び出されるとタイマーが一時停止し、StartTimer()
が呼び出されると再開します。
ベンチマークのベストプラクティスとして、測定対象のコードのみがb.N
ループ内に存在し、かつタイマーが開始されている間に実行されるようにすることが重要です。セットアップやクリーンアップなど、各イテレーションで繰り返されるが測定対象ではない処理は、b.StopTimer()
とb.StartTimer()
の間に配置するか、ループの外に配置する必要があります。
io.Copy
関数
io.Copy(dst Writer, src Reader)
は、Go言語のio
パッケージで提供されるユーティリティ関数で、src
からdst
へデータをコピーします。この関数は、内部的にsrc
がio.WriterTo
インターフェースを実装している場合、またはdst
がio.ReaderFrom
インターフェースを実装している場合に、より効率的なパス(ゼロコピーなど)を使用しようとします。そうでない場合は、内部バッファを使用してデータを読み書きします。
bufio
パッケージ
bufio
パッケージは、I/O操作をバッファリングすることで効率を向上させるための機能を提供します。bufio.Reader
やbufio.Writer
は、それぞれio.Reader
やio.Writer
をラップし、内部バッファを介して読み書きを行うことで、システムコールを減らし、パフォーマンスを改善します。
bytes.Buffer
bytes.Buffer
は、可変長のバイトバッファを実装する型です。io.Reader
およびio.Writer
インターフェースを実装しており、メモリ内でデータを効率的に操作するのに便利です。
windows-amd64-race
ビルダ
GoプロジェクトのCI/CD環境では、様々なプラットフォームや設定でテストが実行されます。windows-amd64-race
は、Windows 64-bit環境でデータ競合検出器(race
フラグ)を有効にしてビルドおよびテストを実行するビルダを指します。race
検出器は、並行処理におけるデータ競合を検出するために追加のインストルメンテーションを行うため、通常よりも多くのメモリやCPUリソースを消費し、実行時間が長くなる傾向があります。そのため、ベンチマークの不正確さがこの環境で特に問題として顕在化しやすかったと考えられます。
技術的詳細
このコミットの技術的な核心は、Goのベンチマークにおけるb.StopTimer()
とb.StartTimer()
の誤用を修正することにあります。以前のベンチマークコードでは、各イテレーションの開始時にb.StopTimer()
を呼び出し、その間にsrc
(ソースリーダー)とdst
(デスティネーションライター)の新しいインスタンスを作成していました。その後、b.StartTimer()
を呼び出してio.Copy
の実行時間を測定していました。
問題は、src
とdst
のインスタンス作成が、単なるポインタの割り当てではなく、内部的にバッファの割り当てや初期化といったO(N)
の作業を伴っていたことです。特にbytes.NewBuffer(make([]byte, 8192))
のように、毎回新しい8KBのバッファを割り当てていたため、これがGCの頻度を増やし、ベンチマークの実行時間に大きな影響を与えていました。b.StopTimer()
で囲まれているにもかかわらず、これらの操作がベンチマークの総実行時間に寄与し、結果としてns/op
の値が不正確になっていました。
修正後のコードでは、src
、dst
、およびそれらの内部バッファ(srcBuf
, dstBuf
)のインスタンスをベンチマークループの外側で一度だけ作成しています。そして、各イテレーションの開始時には、これらの既存のインスタンスをReset()
メソッドを使って再利用しています。
例えば、BenchmarkReaderCopyOptimal
の変更を見てみましょう。
修正前:
func BenchmarkReaderCopyOptimal(b *testing.B) {
// Optimal case is where the underlying reader implements io.WriterTo
for i := 0; i < b.N; i++ {
b.StopTimer()
src := NewReader(bytes.NewBuffer(make([]byte, 8192))) // O(N) work here
dst := onlyWriter{new(bytes.Buffer)} // O(N) work here
b.StartTimer()
io.Copy(dst, src)
}
}
修正後:
func BenchmarkReaderCopyOptimal(b *testing.B) {
// Optimal case is where the underlying reader implements io.WriterTo
srcBuf := bytes.NewBuffer(make([]byte, 8192)) // Setup outside loop
src := NewReader(srcBuf) // Setup outside loop
dstBuf := new(bytes.Buffer) // Setup outside loop
dst := onlyWriter{dstBuf} // Setup outside loop
for i := 0; i < b.N; i++ {
srcBuf.Reset() // Reset existing buffers
src.Reset(srcBuf) // Reset existing readers/writers
dstBuf.Reset()
io.Copy(dst, src) // Only io.Copy is measured
}
}
この変更により、各イテレーションで新しいメモリ割り当てやGCが発生するのを防ぎ、io.Copy
自体のパフォーマンスのみが正確に測定されるようになりました。Reset()
メソッドは、既存のバッファやリーダー/ライターの状態を初期化するだけで、新しいメモリを割り当てないため、オーバーヘッドが大幅に削減されます。
結果として、ns/op
の値が劇的に改善され、ベンチマークの総実行時間も大幅に短縮されました。これは、以前のベンチマークがio.Copy
の性能だけでなく、その前処理(バッファの初期化など)のコストも誤って含んでいたことを明確に示しています。
コアとなるコードの変更箇所
変更はsrc/pkg/bufio/bufio_test.go
ファイルに集中しています。
具体的には、以下のベンチマーク関数が修正されています。
BenchmarkReaderCopyOptimal
BenchmarkReaderCopyUnoptimal
BenchmarkReaderCopyNoWriteTo
BenchmarkWriterCopyOptimal
BenchmarkWriterCopyUnoptimal
BenchmarkWriterCopyNoReadFrom
これらの関数において、b.StopTimer()
とb.StartTimer()
の呼び出しが削除され、代わりにベンチマークループの外部で必要なbytes.Buffer
やbufio.Reader
/bufio.Writer
のインスタンスが初期化されています。そして、ループの各イテレーション内で、これらのインスタンスのReset()
メソッドが呼び出され、再利用されるようになっています。
変更の概要:
b.StopTimer()
とb.StartTimer()
の削除。- ベンチマークループの前に、
srcBuf
,src
,dstBuf
,dst
などの変数を宣言し、一度だけ初期化。 - ベンチマークループの各イテレーション内で、
srcBuf.Reset()
,src.Reset()
,dstBuf.Reset()
,dst.Reset()
などを呼び出して、既存のオブジェクトを再利用し、状態をリセット。
コアとなるコードの解説
各ベンチマーク関数で行われた変更は、基本的に同じパターンに従っています。
例えば、BenchmarkReaderCopyOptimal
では、io.Copy
の最適なケース(io.WriterTo
を実装するリーダー)を測定します。
修正前:
各イテレーションで新しいbufio.Reader
とonlyWriter
(内部にbytes.Buffer
を持つ)が作成されていました。
func BenchmarkReaderCopyOptimal(b *testing.B) {
for i := 0; i < b.N; i++ {
b.StopTimer() // タイマー停止
// ここで新しいReaderとWriterが作成され、メモリが割り当てられる
src := NewReader(bytes.NewBuffer(make([]byte, 8192)))
dst := onlyWriter{new(bytes.Buffer)}
b.StartTimer() // タイマー再開
io.Copy(dst, src)
}
}
修正後:
ループの前にsrcBuf
, src
, dstBuf
, dst
が一度だけ初期化されます。ループ内では、これらの既存のオブジェクトがReset()
メソッドで再利用されます。
func BenchmarkReaderCopyOptimal(b *testing.B) {
srcBuf := bytes.NewBuffer(make([]byte, 8192)) // ループ外で一度だけバッファを確保
src := NewReader(srcBuf) // ループ外で一度だけReaderを確保
dstBuf := new(bytes.Buffer) // ループ外で一度だけバッファを確保
dst := onlyWriter{dstBuf} // ループ外で一度だけWriterを確保
for i := 0; i < b.N; i++ {
srcBuf.Reset() // 既存のsrcBufをリセット
src.Reset(srcBuf) // 既存のsrcをリセット(内部バッファもリセットされる)
dstBuf.Reset() // 既存のdstBufをリセット
// b.StopTimer() / b.StartTimer() は不要になった
io.Copy(dst, src) // io.Copyのみが測定対象となる
}
}
bufio.Reader
とbufio.Writer
のReset()
メソッドは、内部バッファを再利用し、読み書き位置をリセットする機能を提供します。bytes.Buffer
のReset()
も同様に、バッファの内容をクリアし、読み書き位置を先頭に戻しますが、基盤となるメモリは解放しません。これにより、各イテレーションでのメモリ割り当てとGCのオーバーヘッドが排除され、io.Copy
の純粋なパフォーマンスが測定されるようになります。
この修正は、Goのベンチマークを記述する際の重要なベストプラクティスを示しています。すなわち、ベンチマークの測定対象となる操作以外の、各イテレーションで繰り返されるセットアップ処理は、可能な限りループの外で行うか、Reset()
のようなメソッドを使って既存のリソースを再利用すべきであるということです。これにより、ベンチマーク結果の正確性と信頼性が向上します。
関連リンク
- Go言語の
testing
パッケージドキュメント: https://pkg.go.dev/testing - Go言語の
io
パッケージドキュメント: https://pkg.go.dev/io - Go言語の
bufio
パッケージドキュメント: https://pkg.go.dev/bufio - Go言語の
bytes
パッケージドキュメント: https://pkg.go.dev/bytes
参考にした情報源リンク
- Go言語のベンチマークに関する公式ドキュメントやブログ記事 (一般的なベンチマークのベストプラクティスについて)
- Go言語の
testing.B
のStopTimer()
とStartTimer()
の挙動に関する情報 - Go言語の
io.Copy
の内部実装とパフォーマンス特性に関する情報 - Go言語のガベージコレクションとベンチマークへの影響に関する情報
- Go言語のCI/CD環境、特に
race
検出器の挙動に関する情報
(注:具体的なURLは、Web検索の結果に基づいて補完されるべきですが、ここでは一般的な情報源の種類を記載しています。)