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

[インデックス 18070] ファイルの概要

このコミットは、Go言語の標準ライブラリである encoding/json パッケージにおける内部的なオブジェクトプーリングのメカニズムを変更するものです。具体的には、json.Encoder が内部で使用する encodeState オブジェクトの再利用のために、これまで使用していたチャネル(chan)ベースのプールを sync.Pool に置き換えています。

コミット

commit 46b4ed2cf065a9877257c6641e40a0e3cd1468fd
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Wed Dec 18 15:52:05 2013 -0800

    encoding/json: use sync.Pool
    
    Benchmark is within the noise. I had to run this a dozen times
    each before & after (on wall power, without a browser running)
    before I could get halfway consistent numbers, and even then
    they jumped all over the place, with the new one sometimes
    being better. But these are the best of a dozen each.
    
    Slowdown is expected anyway, since I imagine channels are
    optimized more.
    
    benchmark                 old ns/op    new ns/op    delta
    BenchmarkCodeEncoder       26556987     27291072   +2.76%
    BenchmarkEncoderEncode         1069         1071   +0.19%
    
    benchmark                  old MB/s     new MB/s  speedup
    BenchmarkCodeEncoder          73.07        71.10    0.97x
    
    benchmark                old allocs   new allocs    delta
    BenchmarkEncoderEncode            2            2    0.00%
    
    benchmark                 old bytes    new bytes    delta
    BenchmarkEncoderEncode          221          221    0.00%
    
    Update #4720
    
    R=golang-dev, iant
    CC=golang-dev
    https://golang.org/cl/37720047

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/46b4ed2cf065a9877257c6641e40a0e3cd1468fd

元コミット内容

このコミットは、encoding/json パッケージがJSONエンコード処理中に一時的に使用する encodeState 構造体のインスタンスを再利用するためのプーリングメカニズムを、Goのチャネル(chan)から sync.Pool へと変更しています。

コミットメッセージには、変更前後のベンチマーク結果が含まれています。全体的にはわずかなパフォーマンスの低下が見られますが、作者はこれを「ノイズの範囲内」と評価しており、チャネルの方がより最適化されているため、sync.Pool への移行によってわずかな速度低下は予想されていたと述べています。メモリ割り当て(allocs)と割り当てバイト数(bytes)には変化がないことが示されています。

変更の背景

この変更の背景には、Go言語の標準ライブラリにおけるオブジェクトプーリングのベストプラクティスの進化があります。Go 1.3で sync.Pool が導入される以前は、オブジェクトの再利用パターンとして、固定サイズのバッファリングされたチャネルがしばしば用いられていました。これは、ガベージコレクションの負荷を軽減し、パフォーマンスを向上させるための一般的な手法でした。

しかし、チャネルは同期プリミティブであり、オブジェクトの受け渡しにオーバーヘッドが伴います。また、チャネルベースのプールは、プール内のオブジェクト数が固定されるため、負荷の変動に柔軟に対応しにくいという側面がありました。

sync.Pool は、一時的なオブジェクトの再利用に特化して設計されたもので、ガベージコレクタが実行されるとプール内のオブジェクトがクリアされるという特性を持ちます。これは、短命なオブジェクトの再利用に非常に適しており、チャネルよりも低レイテンシでオブジェクトの取得・返却が可能です。

このコミットは、Go 1.2のリリース後、Go 1.3で sync.Pool が導入される前の段階で行われたもので、sync.Pool の導入を見越して、またはその設計思想を反映して、より効率的なプーリングメカニズムへの移行を試みたものと考えられます。コミットメッセージにある Update #4720 は、この変更が特定のIssueに関連していることを示唆しています。Issue 4720は「encoding/json: use sync.Pool for encodeState」というタイトルで、まさにこの変更を提案するものでした。

前提知識の解説

1. Goの encoding/json パッケージ

encoding/json パッケージは、Goのデータ構造とJSONデータの間で変換(エンコードとデコード)を行うための標準ライブラリです。

  • エンコード (Marshal): Goの構造体やマップなどの値をJSON形式のバイト列に変換します。
  • デコード (Unmarshal): JSON形式のバイト列をGoのデータ構造に変換します。
  • json.Encoder: ストリームにJSON値を書き込むための型です。Encode メソッドを呼び出すことで、Goの値をJSONとして出力ストリームに書き込みます。

2. オブジェクトプーリング

オブジェクトプーリングとは、頻繁に生成・破棄されるオブジェクトを再利用可能な状態に保ち、必要に応じてプールから取得し、使用後にプールに戻すことで、オブジェクトの生成とガベージコレクションのオーバーヘッドを削減する手法です。これにより、アプリケーションのパフォーマンスが向上し、メモリ使用量が安定します。

3. Goのチャネル (chan) を用いたプーリング

sync.Pool が導入される前は、Goでオブジェクトプールを実装する一般的な方法の一つとして、バッファリングされたチャネルが使われていました。

var myObjectPool = make(chan *MyObject, 8) // サイズ8のバッファリングされたチャネル

func getMyObject() *MyObject {
    select {
    case obj := <-myObjectPool:
        // プールから取得
        obj.Reset() // 状態をリセット
        return obj
    default:
        // プールが空なら新しく生成
        return &MyObject{}
    }
}

func putMyObject(obj *MyObject) {
    select {
    case myObjectPool <- obj:
        // プールに戻す
    default:
        // プールが満杯なら破棄(ガベージコレクタに任せる)
    }
}

この方式はシンプルですが、チャネル操作のオーバーヘッドや、プールサイズが固定されるという制約がありました。

4. sync.Pool

sync.Pool はGo 1.3で導入された、一時的なオブジェクトの再利用に特化した同期プリミティブです。

import "sync"

var myPool = sync.Pool{
    New: func() interface{} {
        // プールが空の場合に新しいオブジェクトを生成する関数
        return &MyObject{}
    },
}

func getMyObject() *MyObject {
    v := myPool.Get() // プールからオブジェクトを取得
    if v == nil {
        // New関数が設定されていないか、New関数がnilを返した場合
        return &MyObject{} // 新しく生成
    }
    obj := v.(*MyObject)
    obj.Reset() // 状態をリセット
    return obj
}

func putMyObject(obj *MyObject) {
    myPool.Put(obj) // オブジェクトをプールに戻す
}

sync.Pool の主な特徴は以下の通りです。

  • 一時的なオブジェクトの再利用: 主に短命で、頻繁に生成・破棄されるオブジェクトの再利用に適しています。
  • ガベージコレクションとの連携: ガベージコレクタが実行されると、sync.Pool 内のオブジェクトは自動的にクリアされる可能性があります。これは、プールがメモリを無制限に保持しないようにするためです。
  • スレッドセーフ: 複数のGoroutineから安全にアクセスできます。
  • ローカルキャッシュ: 各P(プロセッサ、Goランタイムの内部的な概念)ごとにローカルキャッシュを持ち、ロック競合を減らすことで高速なアクセスを実現します。

技術的詳細

このコミットの技術的な核心は、encoding/json パッケージがJSONエンコード処理中に使用する encodeState オブジェクトの管理方法を、カスタムのチャネルベースのプールから sync.Pool へと移行した点にあります。

encodeState 構造体は、JSONエンコードの際に一時的なバッファや状態を保持するために使用されます。エンコード処理は頻繁に行われるため、encodeState オブジェクトを毎回新しく生成するのではなく、再利用することでガベージコレクションの負荷を軽減し、パフォーマンスを向上させることが期待されます。

変更前 (chan ベースのプール)

変更前は、encodeStatePool という名前のバッファリングされたチャネル(サイズ8)がプールとして機能していました。

// TODO(bradfitz): use a sync.Cache here
var encodeStatePool = make(chan *encodeState, 8)

func newEncodeState() *encodeState {
	select {
	case e := <-encodeStatePool: // プールから取得を試みる
		e.Reset() // 取得したオブジェクトをリセット
		return e
	default:
		return new(encodeState) // プールが空なら新しく生成
	}
}

func putEncodeState(e *encodeState) {
	select {
	case encodeStatePool <- e: // プールに戻す
	default:
		// プールが満杯なら何もしない(オブジェクトはガベージコレクタに回収される)
	}
}

この実装では、newEncodeState でオブジェクトを取得する際に select ステートメントを使用し、チャネルから取得できればそれを再利用し、できなければ新しいオブジェクトを生成します。putEncodeState では、オブジェクトをチャネルに戻そうとしますが、チャネルが満杯の場合はオブジェクトを破棄します。

変更後 (sync.Pool ベースのプール)

変更後は、sync.Pool のインスタンス encodeStatePool が使用されます。

var encodeStatePool sync.Pool

func newEncodeState() *encodeState {
	if v := encodeStatePool.Get(); v != nil { // sync.Poolから取得を試みる
		e := v.(*encodeState)
		e.Reset() // 取得したオブジェクトをリセット
		return e
	}
	return new(encodeState) // プールが空か、取得できなかった場合は新しく生成
}

// func putEncodeState(e *encodeState) は削除され、直接 encodeStatePool.Put(e) が呼ばれる

sync.PoolNew フィールドにオブジェクト生成関数を設定できますが、このコミットでは New フィールドは設定されていません。これは、newEncodeState 関数内で Getnil を返した場合に new(encodeState) を呼び出すことで、新しいオブジェクトの生成をハンドリングしているためです。

オブジェクトをプールに戻す際は、putEncodeState(e) というヘルパー関数が削除され、enc.Encode メソッドの最後で直接 encodeStatePool.Put(e) が呼び出されるようになっています。

パフォーマンスへの影響

コミットメッセージのベンチマーク結果は以下の通りです。

ベンチマーク名old ns/opnew ns/opdeltaold MB/snew MB/sspeedupold allocsnew allocsdeltaold bytesnew bytesdelta
BenchmarkCodeEncoder2655698727291072+2.76%73.0771.100.97x------
BenchmarkEncoderEncode10691071+0.19%---220.00%2212210.00%
  • ns/op (ナノ秒/操作): 処理にかかる時間を示します。値が小さいほど高速です。
    • BenchmarkCodeEncoder は約2.76%の速度低下。
    • BenchmarkEncoderEncode は約0.19%の速度低下。
  • MB/s (メガバイト/秒): 処理スループットを示します。値が大きいほど高速です。
    • BenchmarkCodeEncoder は約0.97倍のスループット低下(つまり、速度低下)。
  • allocs (割り当て数): オブジェクトのメモリ割り当て回数を示します。値が小さいほどガベージコレクションの負荷が低減されます。
  • bytes (バイト数): 割り当てられたメモリのバイト数を示します。

ベンチマーク結果は全体的にわずかな速度低下を示していますが、メモリ割り当て数と割り当てバイト数には変化がありません。これは、sync.Pool への移行が、オブジェクトの再利用という目的自体は達成しているものの、sync.Pool の内部的なオーバーヘッドや、チャネルが特定のシナリオでより最適化されていた可能性を示唆しています。作者が述べているように、「チャネルの方がより最適化されている」という見解は、このベンチマーク結果と一致します。

しかし、sync.Pool はガベージコレクションとの連携や、より柔軟なプールサイズ管理といった点で、チャネルベースのプールよりも優れています。このコミットは、短期的なベンチマーク結果よりも、長期的な設計の健全性や、Goランタイム全体の最適化戦略に沿った変更と見なすことができます。

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

src/pkg/encoding/json/encode.go

--- a/src/pkg/encoding/json/encode.go
+++ b/src/pkg/encoding/json/encode.go
@@ -241,24 +241,15 @@ type encodeState struct {
 	scratch      [64]byte
 }
 
-// TODO(bradfitz): use a sync.Cache here
-var encodeStatePool = make(chan *encodeState, 8)
+var encodeStatePool sync.Pool
 
 func newEncodeState() *encodeState {
-\tselect {\n-\tcase e := <-encodeStatePool:\n+\tif v := encodeStatePool.Get(); v != nil {\n+\t\te := v.(*encodeState)\n \t\te.Reset()\n \t\treturn e
-\tdefault:\n-\t\treturn new(encodeState)\n-\t}\n-}\n-\n-func putEncodeState(e *encodeState) {\n-\tselect {\n-\tcase encodeStatePool <- e:\n-\tdefault:\n \t}\n+\treturn new(encodeState)
 }
 
 func (e *encodeState) marshal(v interface{}) (err error) {

src/pkg/encoding/json/stream.go

--- a/src/pkg/encoding/json/stream.go
+++ b/src/pkg/encoding/json/stream.go
@@ -173,7 +173,7 @@ func (enc *Encoder) Encode(v interface{}) error {
 \tif _, err = enc.w.Write(e.Bytes()); err != nil {\n \t\tenc.err = err\n \t}\n-\tputEncodeState(e)\n+\tencodeStatePool.Put(e)\n \treturn err
 }\n \n```

## コアとなるコードの解説

### `src/pkg/encoding/json/encode.go` の変更

1.  **`encodeStatePool` の型変更**:
    -   変更前: `var encodeStatePool = make(chan *encodeState, 8)`
        -   `encodeState` 型のポインタを格納する、バッファサイズ8のチャネルとして定義されていました。
    -   変更後: `var encodeStatePool sync.Pool`
        -   `sync.Pool` 型の変数として定義されました。これにより、Goランタイムが提供する効率的なオブジェクトプーリングメカニズムを利用します。

2.  **`newEncodeState()` 関数の変更**:
    -   変更前は `select` ステートメントを使用して、`encodeStatePool` チャネルからオブジェクトを取得しようとしていました。チャネルが空の場合は `new(encodeState)` で新しいオブジェクトを生成していました。
    -   変更後:
        ```go
        func newEncodeState() *encodeState {
            if v := encodeStatePool.Get(); v != nil {
                e := v.(*encodeState)
                e.Reset()
                return e
            }
            return new(encodeState)
        }
        ```
        -   `sync.Pool` の `Get()` メソッドを呼び出してプールからオブジェクトを取得します。
        -   `Get()` メソッドは `interface{}` 型を返すため、取得した値 `v` を `*encodeState` 型に型アサーション(`v.(*encodeState)`)しています。
        -   取得したオブジェクトは `e.Reset()` で状態をリセットしてから返されます。これは、再利用されるオブジェクトが以前の状態を引き継がないようにするために重要です。
        -   `Get()` が `nil` を返した場合(プールが空の場合や、`New` フィールドが設定されていない場合)、`new(encodeState)` で新しいオブジェクトが生成されます。

3.  **`putEncodeState()` 関数の削除**:
    -   変更前は `putEncodeState(e *encodeState)` というヘルパー関数が存在し、`select` ステートメントを使ってオブジェクトをチャネルに戻していました。
    -   変更後: この関数は完全に削除されました。オブジェクトをプールに戻す処理は、直接 `sync.Pool.Put()` メソッドを呼び出す形に変更されました。

### `src/pkg/encoding/json/stream.go` の変更

1.  **`Encoder.Encode()` メソッド内の変更**:
    -   変更前: `putEncodeState(e)`
        -   `encodeState` オブジェクト `e` をチャネルベースのプールに戻すために、`putEncodeState` ヘルパー関数を呼び出していました。
    -   変更後: `encodeStatePool.Put(e)`
        -   `sync.Pool` の `Put()` メソッドを直接呼び出して、`encodeState` オブジェクト `e` をプールに戻すように変更されました。

これらの変更により、`encoding/json` パッケージは、Goランタイムが提供するより最適化された `sync.Pool` を利用して、`encodeState` オブジェクトの効率的な再利用を実現しています。これにより、ガベージコレクションの頻度を減らし、全体的なパフォーマンスの向上に寄与することが期待されます。

## 関連リンク

- Go Issue 4720: [encoding/json: use sync.Pool for encodeState](https://github.com/golang/go/issues/4720)
- Go `sync.Pool` ドキュメント: [https://pkg.go.dev/sync#Pool](https://pkg.go.dev/sync#Pool)

## 参考にした情報源リンク

- Go言語の公式ドキュメント
- Goのソースコード(`src/pkg/encoding/json/encode.go`, `src/pkg/encoding/json/stream.go`)
- Goのコミット履歴とIssueトラッカー
- `sync.Pool` の設計に関するGoのブログ記事や関連資料(一般的な知識として)
- Goのチャネルに関する一般的な知識