[インデックス 19589] ファイルの概要
このコミットは、Go言語の標準ライブラリstrings
パッケージ内のbyteReplacer
型が提供するWriteString
メソッドのパフォーマンス改善を目的としています。具体的には、一時的に使用されるバイトバッファの再利用のためにsync.Pool
を導入することで、メモリ割り当てを削減し、ガベージコレクション(GC)の負荷を軽減しています。これにより、WriteString
メソッドの実行速度が向上し、特に大量の文字列置換処理において顕著な効果を発揮します。
コミット
- コミットハッシュ:
3142861ff86a8b4064256f31a0f63dcd23c2f971
- 作者: Rui Ueyama ruiu@google.com
- コミット日時: 2014年6月21日 土曜日 22:08:43 -0700
- 変更ファイル:
src/pkg/strings/replace.go
src/pkg/strings/replace_test.go
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/3142861ff86a8b4064256f31a0f63dcd23c2f971
元コミット内容
strings: use sync.Pool to cache buffer
benchmark old ns/op new ns/op delta
BenchmarkByteReplacerWriteString 3596 3094 -13.96%
benchmark old allocs new allocs delta
BenchmarkByteReplacerWriteString 1 0 -100.00%
LGTM=dvyukov
R=bradfitz, dave, dvyukov
CC=golang-codereviews
https://golang.org/cl/101330053
変更の背景
strings.Replacer
は、複数の文字列置換を効率的に行うためのGo標準ライブラリの機能です。特にWriteString
メソッドは、置換結果を直接io.Writer
に書き込むことで、中間文字列の生成を避けることができます。しかし、このメソッドの内部では、置換処理のために一時的なバイトバッファをmake([]byte, bufsize)
で毎回割り当てていました。
この「毎回割り当て」という動作は、特にWriteString
が頻繁に呼び出されるようなシナリオ(例: 大量のログ出力、HTTPレスポンスの生成など)において、以下のようなパフォーマンス上の問題を引き起こしていました。
- メモリ割り当てのオーバーヘッド: バッファを生成するたびに、Goランタイムはメモリを確保する必要があります。これはCPUサイクルを消費します。
- ガベージコレクション(GC)の負荷: 頻繁に生成される一時的なバッファは、GCの対象となります。GCが頻繁に実行されると、アプリケーションの実行が一時的に停止(GCストップザワールド)し、レイテンシの増加やスループットの低下につながります。
このコミットは、これらの問題を解決し、WriteString
メソッドのパフォーマンスを向上させることを目的としています。ベンチマーク結果が示すように、この変更によりBenchmarkByteReplacerWriteString
の実行時間が約14%短縮され、メモリ割り当てが100%削減されています。
前提知識の解説
sync.Pool
sync.Pool
はGo言語の標準ライブラリsync
パッケージで提供される型で、一時的に使用されるオブジェクトの再利用を目的としたプールです。主な目的は、メモリ割り当てのオーバーヘッドとガベージコレクションの負荷を軽減することにあります。
- 仕組み:
sync.Pool
は、Get()
メソッドでプールからオブジェクトを取得し、Put()
メソッドでオブジェクトをプールに戻します。プールにオブジェクトがない場合、New
フィールドに設定された関数が呼び出され、新しいオブジェクトが生成されます。 - 特徴:
- GC耐性がない:
sync.Pool
に格納されたオブジェクトは、ガベージコレクションの対象となります。つまり、GCが実行されるとプール内のオブジェクトがクリアされる可能性があります。そのため、永続的なオブジェクトの保存には適していません。 - スレッドセーフ: 複数のGoroutineから安全にアクセスできます。
- ローカルプール: 内部的には、各プロセッサ(P)ごとにローカルなプールを持ち、ロック競合を最小限に抑えるように設計されています。
- GC耐性がない:
- 利用シナリオ:
bytes.Buffer
、encoding/json
のencodeState
、I/Oバッファなど、頻繁に生成・破棄される一時的なオブジェクトの再利用に非常に有効です。
io.Writer
io.Writer
はGo言語の標準ライブラリio
パッケージで定義されているインターフェースです。
type Writer interface {
Write(p []byte) (n int, err error)
}
このインターフェースは、バイトスライスを書き込むための単一のWrite
メソッドを定義しています。ファイル、ネットワーク接続、標準出力、bytes.Buffer
など、様々な出力先がこのインターフェースを実装しており、GoのI/O処理における抽象化の基盤となっています。strings.Replacer.WriteString
は、このio.Writer
インターフェースを利用することで、特定の出力先に依存しない汎用的な書き込み機能を提供しています。
メモリ割り当てとガベージコレクション(GC)
Go言語は自動メモリ管理(ガベージコレクション)を採用しています。
- メモリ割り当て: プログラムが新しいデータ構造(例: スライス、マップ、構造体など)を必要とすると、Goランタイムはヒープからメモリを割り当てます。この割り当て処理には一定のコストがかかります。
- ガベージコレクション: 不要になったメモリ(どの変数からも参照されなくなったオブジェクト)を自動的に解放するプロセスです。GCはアプリケーションの実行中にバックグラウンドで動作しますが、GCのフェーズによってはアプリケーションの実行を一時停止させる(ストップザワールド)ことがあります。GCの頻度や実行時間が長くなると、アプリケーションのパフォーマンスに悪影響を与える可能性があります。
sync.Pool
は、これらのメモリ割り当てとGCの負荷を軽減することで、アプリケーションのパフォーマンスを向上させるための重要なツールです。
技術的詳細
このコミットの核心は、strings
パッケージのbyteReplacer
型に属するWriteString
メソッドにおいて、一時的なバイトバッファの管理にsync.Pool
を導入した点です。
変更前は、WriteString
メソッドが呼び出されるたびに、以下のように新しいバイトスライスが作成されていました。
// 変更前
bufsize := 32 << 10 // 32KB
if len(s) < bufsize {
bufsize = len(s)
}
buf := make([]byte, bufsize) // 毎回新しいバッファを割り当て
このmake([]byte, bufsize)
の呼び出しが、前述のメモリ割り当てとGCの負荷の原因となっていました。
変更後、このバッファの割り当てはsync.Pool
によって管理されるようになりました。
-
bufferPool
の定義:var bufferPool = sync.Pool{ New: func() interface{} { b := make([]byte, 4096) // 4KBのバッファを生成 return &b }, }
グローバルな
sync.Pool
インスタンスbufferPool
が定義されました。New
フィールドには、プールが空のときに新しいオブジェクトを生成するための関数が設定されています。ここでは、4KBのバイトスライス([]byte
)を指すポインタ*[]byte
を生成して返しています。ポインタを返すのは、sync.Pool
がinterface{}
型でオブジェクトを扱うため、値のコピーを避けるためと考えられます。 -
WriteString
メソッドでのsync.Pool
の利用:func (r *byteReplacer) WriteString(w io.Writer, s string) (n int, err error) { bp := bufferPool.Get().(*[]byte) // プールからバッファを取得 buf := *bp // ポインタをデリファレンスしてスライスを取得 for len(s) > 0 { ncopy := copy(buf, s) s = s[ncopy:] for i, b := range buf[:ncopy] { buf[i] = r.new[b] } var wn int wn, err = w.Write(buf[:ncopy]) n += wn if err != nil { break } } bufferPool.Put(bp) // バッファをプールに戻す return }
bufferPool.Get().(*[]byte)
:WriteString
が呼び出されると、まずbufferPool
から既存のバイトスライス(のポインタ)を取得しようとします。プールが空であれば、New
関数が呼び出されて新しい4KBのバッファが作成されます。buf := *bp
: 取得したポインタをデリファレンスして、実際のバイトスライスbuf
を取得します。- ループ内での処理: 取得した
buf
を再利用して、文字列のコピーと置換処理を行います。 bufferPool.Put(bp)
:WriteString
メソッドの処理が完了したら、defer
文などを使わずに明示的にbufferPool.Put(bp)
を呼び出して、使用済みのバッファをプールに戻しています。これにより、次のWriteString
呼び出しでこのバッファが再利用される可能性が高まります。
ベンチマーク結果の分析
コミットメッセージに含まれるベンチマーク結果は、この変更の有効性を明確に示しています。
benchmark old ns/op new ns/op delta
BenchmarkByteReplacerWriteString 3596 3094 -13.96%
benchmark old allocs new allocs delta
BenchmarkByteReplacerWriteString 1 0 -100.00%
ns/op
(ナノ秒/操作): 1操作あたりの実行時間を示します。旧バージョンが3596nsだったのに対し、新バージョンは3094nsとなり、約13.96%の高速化が達成されています。これは、バッファの割り当てとGCのオーバーヘッドが削減された直接的な結果です。allocs
(割り当て数): 1操作あたりのメモリ割り当て回数を示します。旧バージョンでは1回割り当てが発生していましたが、新バージョンでは0回に削減されています。これは、sync.Pool
によってバッファが再利用され、新しいメモリ割り当てが不要になったことを意味します。
このベンチマーク結果は、sync.Pool
の導入がstrings.Replacer.WriteString
のパフォーマンス、特にメモリ効率を劇的に改善したことを裏付けています。
コアとなるコードの変更箇所
src/pkg/strings/replace.go
--- a/src/pkg/strings/replace.go
+++ b/src/pkg/strings/replace.go
@@ -4,7 +4,10 @@
package strings
-import "io"
+import (
+ "io"
+ "sync"
+)
// A Replacer replaces a list of strings with replacements.
type Replacer struct {
@@ -451,27 +454,31 @@ func (r *byteReplacer) Replace(s string) string {
return string(buf)
}
-func (r *byteReplacer) WriteString(w io.Writer, s string) (n int, err error) {
- // TODO(bradfitz): use io.WriteString with slices of s, avoiding allocation.
- bufsize := 32 << 10
- if len(s) < bufsize {
- bufsize = len(s)
- }
- buf := make([]byte, bufsize)
+var bufferPool = sync.Pool{
+ New: func() interface{} {
+ b := make([]byte, 4096)
+ return &b
+ },
+}
+func (r *byteReplacer) WriteString(w io.Writer, s string) (n int, err error) {
+ bp := bufferPool.Get().(*[]byte)
+ buf := *bp
+
for len(s) > 0 {
-\t\tncopy := copy(buf, s[:])
-\t\ts = s[ncopy:]
+\t\tncopy := copy(buf, s)
\t\tfor i, b := range buf[:ncopy] {
\t\t\tbuf[i] = r.new[b]
\t\t}\n-\t\twn, err := w.Write(buf[:ncopy])
+\t\ts = s[ncopy:]
+\t\tvar wn int
+\t\twn, err = w.Write(buf[:ncopy])
\t\tn += wn
\t\tif err != nil {\
-\t\t\treturn n, err
+\t\t\tbreak
\t\t}\n \t}\n-\treturn n, nil
+\tbufferPool.Put(bp)
+\treturn
}
// byteStringReplacer is the implementation that\'s used when all the
src/pkg/strings/replace_test.go
テストファイルでは、TestPickAlgorithm
のテストケースがグローバル変数algorithmTestCases
として定義され、TestWriteStringError
という新しいテスト関数が追加されています。
--- a/src/pkg/strings/replace_test.go
+++ b/src/pkg/strings/replace_test.go
@@ -308,20 +308,21 @@ func TestReplacer(t *testing.T) {\n }\n }\n \n+var algorithmTestCases = []struct {\n+\tr *Replacer\n+\twant string\n+}{\n+\t{capitalLetters, "*strings.byteReplacer"},\n+\t{htmlEscaper, "*strings.byteStringReplacer"},\n+\t{NewReplacer("12", "123"), "*strings.singleStringReplacer"},\n+\t{NewReplacer("1", "12"), "*strings.byteStringReplacer"},\n+\t{NewReplacer("", "X"), "*strings.genericReplacer"},\n+\t{NewReplacer("a", "1", "b", "12", "cde", "123"), "*strings.genericReplacer"},\n+}\n+\n // TestPickAlgorithm tests that NewReplacer picks the correct algorithm.\n func TestPickAlgorithm(t *testing.T) {\n-\ttestCases := []struct {\n-\t\tr *Replacer\n-\t\twant string\n-\t}{\n-\t\t{capitalLetters, "*strings.byteReplacer"},\n-\t\t{htmlEscaper, "*strings.byteStringReplacer"},\n-\t\t{NewReplacer("12", "123"), "*strings.singleStringReplacer"},\n-\t\t{NewReplacer("1", "12"), "*strings.byteStringReplacer"},\n-\t\t{NewReplacer("", "X"), "*strings.genericReplacer"},\n-\t\t{NewReplacer("a", "1", "b", "12", "cde", "123"), "*strings.genericReplacer"},\n-\t}\n-\tfor i, tc := range testCases {\n+\tfor i, tc := range algorithmTestCases {\
\t\tgot := fmt.Sprintf("%T", tc.r.Replacer())\n \t\tif got != tc.want {\n \t\t\tt.Errorf("%d. algorithm = %s, want %s", i, got, tc.want)\n@@ -329,6 +330,23 @@ func TestPickAlgorithm(t *testing.T) {\n \t}\n }\n \n+type errWriter struct{}\n+\n+func (errWriter) Write(p []byte) (n int, err error) {\n+\treturn 0, fmt.Errorf("unwritable")\n+}\n+\n+// TestWriteStringError tests that WriteString returns an error\n+// received from the underlying io.Writer.\n+func TestWriteStringError(t *testing.T) {\n+\tfor i, tc := range algorithmTestCases {\n+\t\tn, err := tc.r.WriteString(errWriter{}, "abc")\n+\t\tif n != 0 || err == nil || err.Error() != "unwritable" {\n+\t\t\tt.Errorf("%d. WriteStringError = %d, %v, want 0, unwritable", i, n, err)\n+\t\t}\n+\t}\n+}\n+\n // TestGenericTrieBuilding verifies the structure of the generated trie. There\n // is one node per line, and the key ending with the current line is in the\n // trie if it ends with a "+\".\n```
## コアとなるコードの解説
### `src/pkg/strings/replace.go`の変更点
1. **`sync`パッケージのインポート**:
`import "sync"`が追加され、`sync.Pool`を利用できるようになりました。
2. **`bufferPool`の導入**:
```go
var bufferPool = sync.Pool{
New: func() interface{} {
b := make([]byte, 4096)
return &b
},
}
```
`WriteString`メソッドの外部に、`sync.Pool`型のグローバル変数`bufferPool`が定義されました。このプールは、`WriteString`が一時的に使用するバイトスライスをキャッシュするために使われます。`New`フィールドで指定された関数は、プールが空のときに新しい`[]byte`(サイズ4096バイト、つまり4KB)を生成し、そのポインタを返します。
3. **`WriteString`メソッドの変更**:
- **バッファの取得**:
```go
bp := bufferPool.Get().(*[]byte)
buf := *bp
```
メソッドの冒頭で、`bufferPool.Get()`を呼び出してプールからバイトスライス(のポインタ)を取得します。取得した`interface{}`型を`*[]byte`に型アサートし、さらにデリファレンスして実際のバイトスライス`buf`を取得します。これにより、毎回`make([]byte, ...)`を呼び出す代わりに、既存のバッファを再利用できるようになりました。
- **エラーハンドリングの修正**:
```diff
-\t\t\treturn n, err
+\t\t\tbreak
```
ループ内で`w.Write`がエラーを返した場合の処理が`return n, err`から`break`に変更されています。これは、バッファをプールに戻す処理(`bufferPool.Put(bp)`)が`return`の前に実行されるようにするためです。`defer`を使わない場合、`return`があると`Put`がスキップされてしまうため、このような変更が必要になります。
- **バッファの返却**:
```go
bufferPool.Put(bp)
return
```
メソッドの最後で、使用済みのバイトスライス(のポインタ)を`bufferPool.Put(bp)`でプールに戻しています。これにより、このバッファが将来の`WriteString`呼び出しで再利用される準備が整います。
### `src/pkg/strings/replace_test.go`の変更点
1. **`algorithmTestCases`のグローバル化**:
`TestPickAlgorithm`関数内でローカル変数として定義されていた`testCases`が、グローバル変数`algorithmTestCases`として定義し直されました。これにより、他のテスト関数(新しく追加される`TestWriteStringError`など)からこのテストケースのデータを利用できるようになります。
2. **`TestWriteStringError`の追加**:
この新しいテスト関数は、`WriteString`メソッドが基盤となる`io.Writer`から受け取ったエラーを正しく伝播するかどうかを検証します。
- `errWriter`というカスタムの`io.Writer`実装が定義されており、これは常にエラーを返すように設定されています。
- `algorithmTestCases`の各`Replacer`に対して`WriteString`を呼び出し、返される`n`(書き込まれたバイト数)と`err`が期待通り(`n=0`, `err="unwritable"`)であることを確認しています。これは、`WriteString`メソッドがバッファの再利用ロジックを導入した後も、既存の契約(エラーハンドリング)を維持していることを保証するための重要なテストです。
## 関連リンク
- Go Code Review: [https://golang.org/cl/101330053](https://golang.org/cl/101330053) (このコミットのコードレビューページ)
## 参考にした情報源リンク
- Go `sync.Pool`の目的とパフォーマンス最適化:
- [https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFyZcjm5bC5QKNiNKQ3TvpJ-zkqol1joCTnQbfjMs2f-JZuzZ9pOmfIjWZ3Tqr5FgYA9LWZgHkcnE08BjEx5mP1gAMqn2OJVs5uNJ8VXZOtEL7ZIjRt1ZF-9AD7PM6mMI7IW2EZFZ6QKQ==](https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQFyZcjm5bC5QKNiNKQ3TvpJ-zkqol1joCTnQbfjMs2f-JZuzZ9pOmfIjWZ3Tqr5FgYA9LWZgHkcnE08BjEx5mP1gAMqn2OJVs5uNJ8VXZOtEL7ZIjRt1ZF-9AD7PM6mMI7IW2EZFZ6QKQ==)
- [https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQH9VJQ-hklCvZiTRT6Q7ZBeKg1shNhUJf9MkPCfGMoTvHHkyoDqf9BIeFxwyyfGXWFPdkJOZIzb7fG4zYaQoET_ZMuFXLoL_Yjx6D95yOsshopFw53w7yMKEymMT3Dhm3GQN_IT2eeGEx4=](https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQH9VJQ-hklCvZiTRT6Q7ZBeKg1shNhUJf9MkPCfGMoTvHHkyoDqf9BIeFxwyyfGXWFPdkJOZIzb7fG4zYaQoET_ZMuFXLoL_Yjx6D95yOsshopFw53w7yMKEymMT3Dhm3GQN_IT2eeGEx4=)
- Go `strings.Replacer.WriteString`のパフォーマンス:
- [https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQF6KJLq-AvrPGlcUTiY7fNeJKCpVcr_e7KXh218zJl25Fs97FcfYae0dZ0vg2-V7zx9Za0hGMeapgb9_YjCnz-UE6imCySlDqXW81mODd_4s0-8UujW](https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQF6KJLq-AvrPGlcUTiY7fNeJKCpVcr_e7KXh218zJl25Fs97FcfCae0dZ0vg2-V7zx9Za0hGMeapgb9_YjCnz-UE6imCySlDqXW81mODd_4s0-8UujW)
- [https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHiOatLGu-SYMU3fG2OlueGaK-KdlQis8vG0XIkRwgXs-KVB5yiI0C_f9fPBc_HPDxXpCjkpXJ-x5cVxRW-n0F2sWLESN9wH-huuY8l8KdGDfzx9Y1_CIREaoXuwB06khQjBv66moGC5R4I2ZnW4F_GElSCxekeDGwi6aZ0DDWikmWczgGbFP84TZlLYmvVELbirkWwbzpINQ==](https://vertexaisearch.cloud.google.com/grounding-api-redirect/AUZIYQHiOatLGu-SYMU3fG2OlueGaK-KdlQis8vG0XIkRwgXs-KVB5yiI0C_f9fPBc_HPDxXpCjkpXJ-x5cVxRW-n0F2sWLESN9wH-huuY8l8KdGDfzx9Y1_CIREaoXuwB06khQjBv66moGC5R4I2ZnW4F_GElSCxekeDGwi6aZ0DDWikmWczgGbFP84TZlLYmvVELbirkWwbzpINQ==)