[インデックス 10896] ファイルの概要
このコミットは、Go言語のencoding/binary
パッケージにおけるベンチマークテストの改善および拡張を行ったものです。2011年12月20日にRoger Peppeによって実装され、既存のベンチマークテストをより細分化し、さらに新しいベンチマークテストを追加することで、パフォーマンス測定の精度と比較可能性を向上させました。
コミット
コミット内容: encoding/binary: add more benchmarks
作成者: Roger Peppe rogpeppe@gmail.com
日付: 2011年12月20日 09:25:47 -0800
変更ファイル:
src/pkg/encoding/binary/binary_test.go
(56行追加、17行削除)src/pkg/encoding/binary/varint_test.go
(2行追加)
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/16bf7d9e82fd043ed44b3cb0ebd106c397f326d2
元コミット内容
このコミットは以下の主要な変更を含んでいます:
-
既存ベンチマークの細分化:
BenchmarkRead
→BenchmarkReadInts
BenchmarkWrite
→BenchmarkWriteInts
-
新しいベンチマークの追加:
BenchmarkReadSlice1000Int32s
: 1000個のint32値を含むスライスの読み取り性能測定BenchmarkReadStruct
: 構造体の読み取り性能測定
-
スループット測定の改善:
b.SetBytes()
を使用したバイト数設定による、MB/s単位での性能測定b.ResetTimer()
およびb.StopTimer()
によるタイマー管理の改善
-
varintベンチマークの改善:
- 32ビットおよび64ビットのvarintベンチマークにバイト数設定を追加
変更の背景
2011年当時、Go言語のencoding/binary
パッケージは、バイナリデータの読み書きに関してパフォーマンスの最適化よりもシンプルさを重視した設計が採用されていました。しかし、実際の使用場面では様々な種類のデータ処理が必要となり、それぞれの性能特性を詳細に測定し比較する必要がありました。
このコミットが実装された背景には、以下の要因があります:
-
詳細な性能測定の必要性: 単一のベンチマークテストでは、異なる種類の操作(整数の読み書き、構造体の読み書き、大きなスライスの処理など)の性能特性を正確に把握することが困難でした。
-
比較可能性の向上: 異なるベンチマーク間で結果を比較できるようにするため、統一された測定基準が必要でした。
-
varintベンチマークの正確性: varint(可変長整数)の性能測定において、正確なバイト数の設定が欠如していたため、他のベンチマークとの比較が困難でした。
前提知識の解説
Go言語のベンチマークテスト
Go言語では、testing
パッケージを使用してベンチマークテストを実装します。ベンチマークテストの関数名はBenchmark
で始まり、*testing.B
型の引数を受け取ります。
func BenchmarkExample(b *testing.B) {
for i := 0; i < b.N; i++ {
// 測定対象の処理
}
}
testing.B.SetBytes()メソッド
SetBytes(n int64)
メソッドは、1回の操作で処理されるバイト数を設定し、ベンチマーク結果にスループット(MB/s)を表示させることができます。これにより、ns/op(ナノ秒/操作)とMB/s(メガバイト/秒)の両方の観点から性能を評価できます。
encoding/binaryパッケージの概要
encoding/binary
パッケージは、Goの標準ライブラリの一部として、バイナリデータの読み書きを行うためのツールを提供します。主な特徴は:
- エンディアン対応: BigEndianとLittleEndianの両方に対応
- 型安全性: Goの型システムを活用した安全なデータ変換
- リフレクション使用: 構造体の自動的なシリアライゼーション/デシリアライゼーション
- varint対応: Protocol Buffersと同様の可変長整数エンコーディング
varint(可変長整数)エンコーディング
varintは、小さな数値をより少ないバイト数で表現する可変長エンコーディング方式です。Protocol Buffersで使用されている方式と同じで、各バイトの最上位ビットが継続ビットとして使用されます。
技術的詳細
1. ベンチマークテストの細分化
変更前の問題:
BenchmarkRead
とBenchmarkWrite
が複数の操作を混在させていた- 個別の操作の性能特性を把握することが困難
変更後の改善:
BenchmarkReadInts
とBenchmarkWriteInts
として明確に命名- 整数型の読み書きに特化した測定
2. 新しいベンチマークの追加
BenchmarkReadSlice1000Int32s:
func BenchmarkReadSlice1000Int32s(b *testing.B) {
bsr := &byteSliceReader{}
slice := make([]int32, 1000)
buf := make([]byte, len(slice)*4)
b.SetBytes(int64(len(buf)))
b.ResetTimer()
for i := 0; i < b.N; i++ {
bsr.remain = buf
Read(bsr, BigEndian, slice)
}
}
この関数は、1000個のint32値(4000バイト)を含むスライスの読み取り性能を測定します。
BenchmarkReadStruct:
func BenchmarkReadStruct(b *testing.B) {
bsr := &byteSliceReader{}
var buf bytes.Buffer
Write(&buf, BigEndian, &s)
n := TotalSize(reflect.ValueOf(s))
b.SetBytes(int64(n))
t := s
b.ResetTimer()
for i := 0; i < b.N; i++ {
bsr.remain = buf.Bytes()
Read(bsr, BigEndian, &t)
}
b.StopTimer()
if !reflect.DeepEqual(s, t) {
panic("no match")
}
}
この関数は、構造体の読み取り性能を測定し、結果の正確性も検証します。
3. タイマー管理の改善
全てのベンチマークで以下のタイマー管理が適用されています:
b.ResetTimer()
: 初期化処理を除外b.StopTimer()
: 検証処理を除外
これにより、純粋な読み書き操作の性能のみを測定できます。
4. varintベンチマークの改善
func BenchmarkPutUvarint32(b *testing.B) {
buf := make([]byte, MaxVarintLen32)
b.SetBytes(4) // 新しく追加
for i := 0; i < b.N; i++ {
for j := uint(0); j < MaxVarintLen32; j++ {
PutUvarint(buf, 1<<(j*7))
}
}
}
32ビットのvarintは4バイト相当、64ビットのvarintは8バイト相当として設定されています。
コアとなるコードの変更箇所
1. binary_test.go の主要変更
関数名の変更:
BenchmarkRead
→BenchmarkReadInts
BenchmarkWrite
→BenchmarkWriteInts
新しいベンチマーク関数の追加:
BenchmarkReadSlice1000Int32s
BenchmarkReadStruct
バイト数設定の追加:
b.SetBytes(2 * (1 + 2 + 4 + 8)) // 整数型のサイズ合計
2. WriteIntsベンチマークの修正
変更前:
Write(w, BigEndian, &s.Int8) // ポインタ渡し
Write(w, BigEndian, &s.Int16)
// ...
変更後:
Write(w, BigEndian, s.Int8) // 値渡し
Write(w, BigEndian, s.Int16)
// ...
この変更により、ポインタのデリファレンスのオーバーヘッドが除去されています。
3. 検証ロジックの簡素化
変更前:
if !bytes.Equal(buf.Bytes()[:30], big[:30]) {
panic("first half doesn't match")
}
if !bytes.Equal(buf.Bytes()[30:], big[:30]) {
panic("second half doesn't match")
}
変更後:
if !bytes.Equal(buf.Bytes(), big[:30]) {
panic("first half doesn't match")
}
重複していた検証ロジックが簡素化されています。
コアとなるコードの解説
1. byteSliceReader の活用
type byteSliceReader struct {
remain []byte
}
func (br *byteSliceReader) Read(p []byte) (int, error) {
n := copy(p, br.remain)
br.remain = br.remain[n:]
return n, nil
}
この構造体は、バイトスライスからの読み取りを効率的に行うためのカスタムリーダーです。メモリ上のデータを直接操作するため、ファイルI/Oなどの外部要因を排除してベンチマークを実行できます。
2. SetBytes による性能測定の標準化
b.SetBytes(int64(len(buf)))
各ベンチマークでSetBytesを呼び出すことで、以下の利点があります:
- 統一された測定基準(MB/s)による比較
- 操作の効率性の可視化
- 異なるデータサイズでの性能特性の把握
3. TotalSize関数の使用
n := TotalSize(reflect.ValueOf(s))
この関数は、構造体のバイナリ表現における総サイズを計算します。リフレクションを使用して動的にサイズを取得するため、構造体の定義変更に対して柔軟に対応できます。
関連リンク
- Go Documentation - encoding/binary
- Go Documentation - testing
- Go Source Code - encoding/binary
- Protocol Buffers - Variable-length encoding