[インデックス 13863] ファイルの概要
このコミットは、Go言語の標準ライブラリであるbytes
パッケージとstrings
パッケージのFields
およびFieldsFunc
関数のベンチマークを追加するものです。これにより、これらの関数のパフォーマンス特性を測定し、将来的な最適化のベースラインを確立することを目的としています。
コミット
commit 0e60019a42b6c5c98ddb6b6418481133e3c42854
Author: Russ Cox <rsc@golang.org>
Date: Tue Sep 18 15:02:08 2012 -0400
bytes, strings: add Fields benchmarks
The performance changes will be a few different CLs.
Start with benchmarks as a baseline.
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/6537043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/0e60019a42b6c5c98ddb6b6418481133e3c42854
元コミット内容
bytes, strings: add Fields benchmarks
このコミットは、bytes
パッケージとstrings
パッケージにFields
関数のベンチマークを追加します。
パフォーマンスの変更はいくつかの異なる変更リスト(CLs)で行われる予定です。
ベンチマークをベースラインとして開始します。
変更の背景
Go言語の標準ライブラリは、そのパフォーマンスと効率性で知られています。bytes.Fields
とstrings.Fields
は、それぞれバイトスライスと文字列を空白文字で分割するための基本的なユーティリティ関数です。これらの関数は、テキスト処理において頻繁に使用されるため、そのパフォーマンスはアプリケーション全体の性能に大きな影響を与える可能性があります。
このコミットの背景には、これらの関数のパフォーマンスを改善するという長期的な目標があります。しかし、パフォーマンス改善を行う前に、現在の実装の性能を正確に測定し、その後の変更が実際に性能向上に寄与しているかを客観的に評価するための「ベースライン」が必要です。ベンチマークを追加することで、将来の変更が意図しない性能劣化を引き起こさないかを確認し、また、どの程度の性能向上が達成されたかを数値で示すことが可能になります。
コミットメッセージにある「The performance changes will be a few different CLs. Start with benchmarks as a baseline.」という記述は、このコミットが単なるベンチマークの追加であり、実際のパフォーマンス最適化は後続のコミットで行われることを明確に示しています。これは、Go言語の開発における一般的なプラクティスであり、変更を小さく保ち、各変更の目的を明確にすることで、コードレビューを容易にし、バグの導入リスクを低減します。
前提知識の解説
Go言語のtesting
パッケージとベンチマーク
Go言語には、標準ライブラリとしてtesting
パッケージが提供されており、ユニットテスト、例(Example)テスト、そしてベンチマークテストを記述するための機能が含まれています。
- ベンチマークテスト: Goのベンチマークテストは、関数の実行にかかる時間やメモリ割り当てを測定するために使用されます。テストファイル(
_test.go
で終わるファイル)内にBenchmarkXxx
という命名規則に従って関数を記述します。func BenchmarkXxx(b *testing.B)
: ベンチマーク関数は*testing.B
型の引数を取ります。b.N
: ベンチマーク関数内のループはb.N
回実行されます。b.N
の値は、ベンチマーク実行時にgo test
コマンドによって動的に調整され、測定の信頼性を高めるために十分な回数実行されるように制御されます。b.SetBytes(int64(len(input)))
: このメソッドは、ベンチマーク対象の操作が処理するバイト数をtesting
パッケージに伝えます。これにより、結果として「N op/s」(1秒あたりの操作数)だけでなく、「N MB/s」(1秒あたりの処理メガバイト数)のような、より意味のあるスループット指標も表示されるようになります。go test -bench=.
: ベンチマークを実行するためのコマンドです。
bytes.Fields
とstrings.Fields
bytes.Fields(s []byte) [][]byte
:bytes
パッケージのFields
関数は、バイトスライスs
を一つ以上の連続するUnicodeの空白文字(unicode.IsSpace
で定義される)で区切られた部分スライスに分割し、その部分スライスのスライスを返します。空白文字は結果に含まれません。strings.Fields(s string) []string
:strings
パッケージのFields
関数は、文字列s
を一つ以上の連続するUnicodeの空白文字で区切られた部分文字列に分割し、その部分文字列のスライスを返します。空白文字は結果に含まれません。
これらの関数は、内部的にはFieldsFunc
を呼び出しており、unicode.IsSpace
を区切り文字の判定関数として使用しています。
bytes.FieldsFunc
とstrings.FieldsFunc
bytes.FieldsFunc(s []byte, f func(rune) bool) [][]byte
:bytes
パッケージのFieldsFunc
関数は、バイトスライスs
を、指定された関数f
がtrue
を返すUnicode文字で区切られた部分スライスに分割します。strings.FieldsFunc(s string, f func(rune) bool) []string
:strings
パッケージのFieldsFunc
関数は、文字列s
を、指定された関数f
がtrue
を返すUnicode文字で区切られた部分文字列に分割します。
これらの関数は、カスタムの区切り文字ロジックを適用できるため、より柔軟なテキスト分割が可能です。
UnicodeとUTF-8
Go言語はUnicodeを完全にサポートしており、文字列はUTF-8エンコーディングで内部的に表現されます。Fields
やFieldsFunc
のような関数がUnicodeの空白文字を正しく処理できるのは、この設計によるものです。unicode.IsSpace
は、Unicodeの定義する様々な空白文字(通常のスペース、タブ、改行、ノーブレークスペースなど)を識別します。
技術的詳細
このコミットで追加されたベンチマークは、bytes.Fields
、bytes.FieldsFunc
、strings.Fields
、strings.FieldsFunc
の4つの関数を対象としています。ベンチマークの設計において重要なのは、現実的な入力データを用意することです。
makeFieldsInput
関数
makeFieldsInput
関数は、ベンチマークの入力データを生成するために導入されました。この関数は、約1MB(1<<20
バイト)のバイトスライス(bytes
パッケージ用)または文字列(strings
パッケージ用)を生成します。
入力データの特性は以下の通りです。
- 約10%がスペース文字(
' '
)。 - 約10%が2バイトのUTF-8文字(
'χ'
)。 - 残りの約80%がASCIIの非スペース文字(
'x'
)。
この混合されたデータセットは、実際のテキストデータに近い多様な文字種と空白文字の分布をシミュレートしており、ベンチマーク結果の現実性を高めます。特に、UTF-8文字の存在は、Goの文字列処理がUnicodeを考慮していることを反映しており、マルチバイト文字の処理性能も測定対象となります。
rand.Intn(10)
を使用して、各バイト(またはルーン)がスペース、UTF-8文字、またはASCII文字のいずれになるかをランダムに決定しています。copy(x[i-1:], "χ")
の部分は、2バイトのUTF-8文字を正しく挿入するための処理です。
ベンチマーク関数
BenchmarkFields
とBenchmarkFieldsFunc
は、それぞれFields
とFieldsFunc
のパフォーマンスを測定します。
b.SetBytes(int64(len(fieldsInput)))
: この行は、各ベンチマーク実行で処理されるバイト数をtesting
パッケージに通知します。これにより、go test -bench=.
を実行した際に、処理速度が「ops/s」(1秒あたりの操作数)だけでなく、「MB/s」(1秒あたりのメガバイト数)としても表示され、スループットの評価が容易になります。for i := 0; i < b.N; i++
: このループ内で、対象の関数(Fields
またはFieldsFunc
)がfieldsInput
に対して呼び出されます。b.N
はgo test
コマンドによって自動的に調整され、統計的に有意な結果が得られるように十分な回数実行されます。
FieldsFunc
のベンチマークでは、unicode.IsSpace
を述語関数として使用しています。これは、Fields
関数が内部的に行っている処理と同じであり、Fields
とFieldsFunc
の基本的なパフォーマンス比較を可能にします。
TestFieldsFunc
の追加テストケース
bytes_test.go
とstrings_test.go
の両方で、既存のTestFieldsFunc
に新しいテストケースが追加されています。これは、FieldsFunc
がunicode.IsSpace
を述語として使用した場合に、Fields
と同じ結果を返すことを確認するためのものです。これにより、ベンチマークの前提となる関数の振る舞いが正しいことが保証されます。
コアとなるコードの変更箇所
このコミットでは、主に以下の2つのファイルにコードが追加されています。
src/pkg/bytes/bytes_test.go
src/pkg/strings/strings_test.go
それぞれのファイルに、makeFieldsInput
関数、fieldsInput
変数、BenchmarkFields
関数、BenchmarkFieldsFunc
関数が追加されています。また、既存のTestFieldsFunc
にFields
のテストケースを流用したテストが追加されています。
src/pkg/bytes/bytes_test.go
の変更点
--- a/src/pkg/bytes/bytes_test.go
+++ b/src/pkg/bytes/bytes_test.go
@@ -6,6 +6,7 @@ package bytes_test
import (
. "bytes"
+ "math/rand"
"reflect"
"testing"
"unicode"
@@ -567,6 +568,14 @@ func TestFields(t *testing.T) {
}
func TestFieldsFunc(t *testing.T) {
+ for _, tt := range fieldstests {
+ a := FieldsFunc([]byte(tt.s), unicode.IsSpace)
+ result := arrayOfString(a)
+ if !eq(result, tt.a) {
+ t.Errorf("FieldsFunc(%q, unicode.IsSpace) = %v; want %v", tt.s, a, tt.a)
+ continue
+ }
+ }
pred := func(c rune) bool { return c == 'X' }
var fieldsFuncTests = []FieldsTest{
{"", []string{}},
@@ -1014,3 +1023,39 @@ func TestEqualFold(t *testing.T) {
}
}\n }\n+\n+var makeFieldsInput = func() []byte {\n+\tx := make([]byte, 1<<20)\n+\t// Input is ~10% space, ~10% 2-byte UTF-8, rest ASCII non-space. \n+\tfor i := range x {\n+\t\tswitch rand.Intn(10) {\n+\t\tcase 0:\n+\t\t\tx[i] = ' '\n+\t\tcase 1:\n+\t\t\tif i > 0 && x[i-1] == 'x' {\n+\t\t\t\tcopy(x[i-1:], "χ")\n+\t\t\t\tbreak\n+\t\t\t}\n+\t\t\tfallthrough\n+\t\tdefault:\n+\t\t\tx[i] = 'x'\n+\t\t}\n+\t}\n+\treturn x\n+}\n+\n+var fieldsInput = makeFieldsInput()\n+\n+func BenchmarkFields(b *testing.B) {\n+\tb.SetBytes(int64(len(fieldsInput)))\n+\tfor i := 0; i < b.N; i++ {\n+\t\tFields(fieldsInput)\n+\t}\n+}\n+\n+func BenchmarkFieldsFunc(b *testing.B) {\n+\tb.SetBytes(int64(len(fieldsInput)))\n+\tfor i := 0; i < b.N; i++ {\n+\t\tFieldsFunc(fieldsInput, unicode.IsSpace)\n+\t}\n+}\n```
### `src/pkg/strings/strings_test.go` の変更点
```diff
--- a/src/pkg/strings/strings_test.go
+++ b/src/pkg/strings/strings_test.go
@@ -7,6 +7,7 @@ package strings_test
import (
"bytes"
"io"
+ "math/rand"
"reflect"
. "strings"
"testing"
@@ -311,6 +312,13 @@ var FieldsFuncTests = []FieldsTest{
}
func TestFieldsFunc(t *testing.T) {
+ for _, tt := range fieldstests {
+ a := FieldsFunc(tt.s, unicode.IsSpace)
+ if !eq(a, tt.a) {
+ t.Errorf("FieldsFunc(%q, unicode.IsSpace) = %v; want %v", tt.s, a, tt.a)
+ continue
+ }
+ }
pred := func(c rune) bool { return c == 'X' }
for _, tt := range FieldsFuncTests {
\ta := FieldsFunc(tt.s, pred)
@@ -984,3 +992,39 @@ func TestEqualFold(t *testing.T) {\n }\n }\n }\n+\n+var makeFieldsInput = func() string {\n+\tx := make([]byte, 1<<20)\n+\t// Input is ~10% space, ~10% 2-byte UTF-8, rest ASCII non-space. \n+\tfor i := range x {\n+\t\tswitch rand.Intn(10) {\n+\t\tcase 0:\n+\t\t\tx[i] = ' '\n+\t\tcase 1:\n+\t\t\tif i > 0 && x[i-1] == 'x' {\n+\t\t\t\tcopy(x[i-1:], "χ")\n+\t\t\t\tbreak\n+\t\t\t}\n+\t\t\tfallthrough\n+\t\tdefault:\n+\t\t\tx[i] = 'x'\n+\t\t}\n+\t}\n+\treturn string(x)\n+}\n+\n+var fieldsInput = makeFieldsInput()\n+\n+func BenchmarkFields(b *testing.B) {\n+\tb.SetBytes(int64(len(fieldsInput)))\n+\tfor i := 0; i < b.N; i++ {\n+\t\tFields(fieldsInput)\n+\t}\n+}\n+\n+func BenchmarkFieldsFunc(b *testing.B) {\n+\tb.SetBytes(int64(len(fieldsInput)))\n+\tfor i := 0; i < b.N; i++ {\n+\t\tFieldsFunc(fieldsInput, unicode.IsSpace)\n+\t}\n+}\n```
## コアとなるコードの解説
### `makeFieldsInput` 関数
この関数は、ベンチマークの入力データを生成します。
* `x := make([]byte, 1<<20)`: 1MBのバイトスライスを初期化します。
* ループ内で、`rand.Intn(10)`を使ってランダムに文字を生成します。
* `case 0`: スペース文字 `' '` を挿入します(約10%の確率)。
* `case 1`: 2バイトのUTF-8文字であるギリシャ文字のカイ `'χ'` を挿入します(約10%の確率)。`copy(x[i-1:], "χ")`は、UTF-8文字が2バイトを占めるため、前のバイトからコピーを開始して正しく挿入するための処理です。
* `default`: ASCII文字 `'x'` を挿入します(約80%の確率)。
* `bytes_test.go`では`[]byte`をそのまま返し、`strings_test.go`では`string(x)`で文字列に変換して返します。
この関数によって生成される`fieldsInput`は、ベンチマークの各実行で再利用され、ベンチマークのセットアップ時間を測定から除外します。
### `BenchmarkFields` 関数
`bytes.Fields`および`strings.Fields`関数のパフォーマンスを測定します。
* `b.SetBytes(int64(len(fieldsInput)))`: 処理される入力データのバイトサイズを設定します。これにより、ベンチマーク結果にスループット(MB/s)が表示されるようになります。
* `for i := 0; i < b.N; i++`: ベンチマークループです。`b.N`回、対象の`Fields`関数が`fieldsInput`に対して呼び出されます。
### `BenchmarkFieldsFunc` 関数
`bytes.FieldsFunc`および`strings.FieldsFunc`関数のパフォーマンスを測定します。
* `b.SetBytes(int64(len(fieldsInput)))`: 同様に処理される入力データのバイトサイズを設定します。
* `for i := 0; i < b.N; i++`: ベンチマークループです。`b.N`回、対象の`FieldsFunc`関数が`fieldsInput`と`unicode.IsSpace`述語に対して呼び出されます。
### `TestFieldsFunc` の追加テストケース
既存の`TestFieldsFunc`内に、`fieldstests`(`Fields`関数のテストケース)を流用した新しいループが追加されています。
* `a := FieldsFunc([]byte(tt.s), unicode.IsSpace)`: `FieldsFunc`を`unicode.IsSpace`述語と共に呼び出します。
* `if !eq(result, tt.a)`: `FieldsFunc`の結果が、対応する`Fields`の期待される結果(`tt.a`)と一致するかを検証します。
この追加テストは、`FieldsFunc`が`unicode.IsSpace`を述語として使用した場合に、`Fields`関数と論理的に同等であることを保証します。これにより、ベンチマークの対象となる関数の振る舞いが期待通りであることが確認できます。
## 関連リンク
* Go言語の`testing`パッケージのドキュメント: [https://pkg.go.dev/testing](https://pkg.go.dev/testing)
* Go言語の`bytes`パッケージのドキュメント: [https://pkg.go.dev/bytes](https://pkg.go.dev/bytes)
* Go言語の`strings`パッケージのドキュメント: [https://pkg.go.dev/strings](https://pkg.go.dev/strings)
* Go言語の`unicode`パッケージのドキュメント: [https://pkg.go.dev/unicode](https://pkg.go.dev/unicode)
* Go言語のベンチマークに関する公式ブログ記事 (古いですが概念は同じ): [https://go.dev/blog/benchmarking](https://go.dev/blog/benchmarking)
## 参考にした情報源リンク
* Go言語の公式ドキュメント
* Go言語のソースコード
* Go言語のベンチマークに関する一般的な知識
* UnicodeとUTF-8エンコーディングに関する一般的な知識