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

[インデックス 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.Fieldsstrings.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.Fieldsstrings.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.FieldsFuncstrings.FieldsFunc

  • bytes.FieldsFunc(s []byte, f func(rune) bool) [][]byte: bytesパッケージのFieldsFunc関数は、バイトスライスsを、指定された関数ftrueを返すUnicode文字で区切られた部分スライスに分割します。
  • strings.FieldsFunc(s string, f func(rune) bool) []string: stringsパッケージのFieldsFunc関数は、文字列sを、指定された関数ftrueを返すUnicode文字で区切られた部分文字列に分割します。

これらの関数は、カスタムの区切り文字ロジックを適用できるため、より柔軟なテキスト分割が可能です。

UnicodeとUTF-8

Go言語はUnicodeを完全にサポートしており、文字列はUTF-8エンコーディングで内部的に表現されます。FieldsFieldsFuncのような関数がUnicodeの空白文字を正しく処理できるのは、この設計によるものです。unicode.IsSpaceは、Unicodeの定義する様々な空白文字(通常のスペース、タブ、改行、ノーブレークスペースなど)を識別します。

技術的詳細

このコミットで追加されたベンチマークは、bytes.Fieldsbytes.FieldsFuncstrings.Fieldsstrings.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文字を正しく挿入するための処理です。

ベンチマーク関数

BenchmarkFieldsBenchmarkFieldsFuncは、それぞれFieldsFieldsFuncのパフォーマンスを測定します。

  • 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.Ngo testコマンドによって自動的に調整され、統計的に有意な結果が得られるように十分な回数実行されます。

FieldsFuncのベンチマークでは、unicode.IsSpaceを述語関数として使用しています。これは、Fields関数が内部的に行っている処理と同じであり、FieldsFieldsFuncの基本的なパフォーマンス比較を可能にします。

TestFieldsFuncの追加テストケース

bytes_test.gostrings_test.goの両方で、既存のTestFieldsFuncに新しいテストケースが追加されています。これは、FieldsFuncunicode.IsSpaceを述語として使用した場合に、Fieldsと同じ結果を返すことを確認するためのものです。これにより、ベンチマークの前提となる関数の振る舞いが正しいことが保証されます。

コアとなるコードの変更箇所

このコミットでは、主に以下の2つのファイルにコードが追加されています。

  1. src/pkg/bytes/bytes_test.go
  2. src/pkg/strings/strings_test.go

それぞれのファイルに、makeFieldsInput関数、fieldsInput変数、BenchmarkFields関数、BenchmarkFieldsFunc関数が追加されています。また、既存のTestFieldsFuncFieldsのテストケースを流用したテストが追加されています。

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エンコーディングに関する一般的な知識