[インデックス 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.Pool
は New
フィールドにオブジェクト生成関数を設定できますが、このコミットでは New
フィールドは設定されていません。これは、newEncodeState
関数内で Get
が nil
を返した場合に new(encodeState)
を呼び出すことで、新しいオブジェクトの生成をハンドリングしているためです。
オブジェクトをプールに戻す際は、putEncodeState(e)
というヘルパー関数が削除され、enc.Encode
メソッドの最後で直接 encodeStatePool.Put(e)
が呼び出されるようになっています。
パフォーマンスへの影響
コミットメッセージのベンチマーク結果は以下の通りです。
ベンチマーク名 | old ns/op | new ns/op | delta | old MB/s | new MB/s | speedup | old allocs | new allocs | delta | old bytes | new bytes | delta |
---|---|---|---|---|---|---|---|---|---|---|---|---|
BenchmarkCodeEncoder | 26556987 | 27291072 | +2.76% | 73.07 | 71.10 | 0.97x | - | - | - | - | - | - |
BenchmarkEncoderEncode | 1069 | 1071 | +0.19% | - | - | - | 2 | 2 | 0.00% | 221 | 221 | 0.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のチャネルに関する一般的な知識