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

[インデックス 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レスポンスの生成など)において、以下のようなパフォーマンス上の問題を引き起こしていました。

  1. メモリ割り当てのオーバーヘッド: バッファを生成するたびに、Goランタイムはメモリを確保する必要があります。これはCPUサイクルを消費します。
  2. ガベージコレクション(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)ごとにローカルなプールを持ち、ロック競合を最小限に抑えるように設計されています。
  • 利用シナリオ: bytes.Bufferencoding/jsonencodeState、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によって管理されるようになりました。

  1. bufferPoolの定義:

    var bufferPool = sync.Pool{
        New: func() interface{} {
            b := make([]byte, 4096) // 4KBのバッファを生成
            return &b
        },
    }
    

    グローバルなsync.PoolインスタンスbufferPoolが定義されました。Newフィールドには、プールが空のときに新しいオブジェクトを生成するための関数が設定されています。ここでは、4KBのバイトスライス([]byte)を指すポインタ*[]byteを生成して返しています。ポインタを返すのは、sync.Poolinterface{}型でオブジェクトを扱うため、値のコピーを避けるためと考えられます。

  2. 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==)