[インデックス 16298] ファイルの概要
このコミットは、Go言語の標準ライブラリである encoding/json
パッケージにおける Encoder
のメモリ割り当てを削減することを目的としています。具体的には、JSONエンコーディング処理中に一時的に使用される encodeState
オブジェクトの再利用メカニズムを導入することで、ガベージコレクションの負荷を軽減し、パフォーマンスを向上させています。
コミット
commit f1583bb9563827fe132c97798657a6c60e6a0457
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Tue May 14 15:50:46 2013 -0700
encoding/json: allocate less in NewEncoder
The *Encoder is almost always garbage. It doesn't need an
encodeState inside of it (and its bytes.Buffer), since it's
only needed locally inside of Encode.
benchmark old ns/op new ns/op delta
BenchmarkEncoderEncode 2562 2553 -0.35%
benchmark old bytes new bytes delta
BenchmarkEncoderEncode 283 102 -63.96%
R=r
CC=gobot, golang-dev
https://golang.org/cl/9365044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f1583bb9563827fe132c97798657a6c60e6a0457
元コミット内容
encoding/json
パッケージの Encoder
が encodeState
オブジェクトを内部に保持していたため、Encoder
が生成されるたびに encodeState
も一緒に割り当てられていました。しかし、encodeState
は Encode
メソッドの実行中にのみ必要とされる一時的なオブジェクトであり、Encoder
自体は頻繁に再利用されることが少ないため、encodeState
の割り当てが不要なガベージを生成していました。このコミットは、encodeState
を Encoder
の内部から切り離し、必要に応じてプールから取得・返却するメカニズムを導入することで、メモリ割り当てを削減し、ガベージコレクションのオーバーヘッドを低減することを目的としています。
ベンチマーク結果は以下の通りです。
BenchmarkEncoderEncode
の実行時間はわずかに改善(-0.35%)BenchmarkEncoderEncode
のメモリ割り当ては大幅に削減(-63.96%)
変更の背景
Go言語では、ガベージコレクション(GC)が自動的にメモリ管理を行いますが、頻繁なオブジェクトの生成と破棄はGCの負荷を増大させ、アプリケーションのパフォーマンスに影響を与える可能性があります。特に、ループ内で繰り返し生成される一時的なオブジェクトは、GCの頻度を高め、レイテンシースパイクを引き起こす原因となります。
encoding/json
パッケージの Encoder
は、JSONエンコーディングを行う際に内部で encodeState
という構造体を使用します。この encodeState
は、エンコーディング処理の途中で必要なバッファ(bytes.Buffer
)や一時的なスクラッチ領域を保持しています。コミットメッセージにあるように、これまでの実装では *Encoder
が生成されるたびに encodeState
も一緒に割り当てられていました。しかし、*Encoder
自体は一度作成されると複数の Encode
呼び出しで再利用されることがありますが、encodeState
は各 Encode
呼び出しのたびにリセットされて使用される一時的なデータであり、Encoder
のライフサイクル全体にわたって保持する必要はありませんでした。
この設計は、Encoder
が頻繁に生成・破棄されるようなシナリオ(例: 各HTTPリクエストごとに新しい Encoder
を作成する場合)において、encodeState
とその内部バッファの不要なメモリ割り当てと解放を繰り返し発生させ、ガベージコレクションの負担を増やしていました。このコミットは、この非効率性を解消し、メモリ割り当てを削減することで、アプリケーション全体のパフォーマンス、特にレイテンシーの安定性向上を目指しています。
前提知識の解説
Go言語のガベージコレクション (GC)
Go言語は、自動メモリ管理のためにトレース型ガベージコレクタを採用しています。開発者は明示的にメモリを解放する必要がなく、GCが不要になったオブジェクトを自動的に回収します。しかし、GCは完全にオーバーヘッドがないわけではありません。GCが動作する際には、アプリケーションの実行が一時的に停止(ストップ・ザ・ワールド)したり、CPUリソースを消費したりすることがあります。特に、ヒープ上に大量のオブジェクトが頻繁に割り当てられると、GCの頻度が増加し、アプリケーションのパフォーマンスに悪影響を与える可能性があります。
オブジェクトプール (Object Pool)
オブジェクトプールは、頻繁に生成・破棄されるオブジェクトのメモリ割り当てと解放のオーバーヘッドを削減するためのデザインパターンです。オブジェクトを使い終わった後すぐに解放するのではなく、再利用可能なオブジェクトの「プール」に戻します。次に同じ種類のオブジェクトが必要になったときには、新しく割り当てる代わりにプールから既存のオブジェクトを取得して再利用します。これにより、GCの負荷を軽減し、パフォーマンスを向上させることができます。
Go言語では、sync.Pool
パッケージが汎用的なオブジェクトプールを提供しています。これは、一時的なオブジェクトの再利用に非常に効果的です。このコミットでは、sync.Pool
が導入される前のGoのバージョンであるため、カスタムのチャネルベースのプール (encodeStatePool
) が実装されています。
encoding/json
パッケージの Encoder
と encodeState
encoding/json
パッケージは、Goのデータ構造とJSONデータの間で変換を行うための機能を提供します。
json.Encoder
: Goの値をJSON形式でストリームに書き込むための型です。NewEncoder
関数で作成され、Encode
メソッドを呼び出すことで値のエンコーディングを行います。encodeState
:Encoder
の内部で使用されるヘルパー構造体です。JSONエンコーディングの過程で必要な状態(例:bytes.Buffer
を使った出力バッファ、一時的なバイト配列scratch
)を保持します。これまでの実装では、Encoder
のフィールドとしてencodeState
が含まれていました。
技術的詳細
このコミットの主要な変更点は、encodeState
オブジェクトのライフサイクル管理を Encoder
から分離し、オブジェクトプール(チャネルベースのカスタム実装)を導入したことです。
-
encodeState
の分離: 以前はjson.Encoder
構造体の中にencodeState
のインスタンスが直接埋め込まれていました。type Encoder struct { w io.Writer err error e encodeState // 以前はここにencodeStateがあった }
この変更により、
Encoder
はencodeState
を直接保持しなくなりました。代わりに、Encode
メソッドが呼び出されるたびに、プールからencodeState
を取得し、処理が完了したらプールに返却するようになりました。 -
encodeStatePool
の導入:src/pkg/encoding/json/encode.go
にencodeStatePool
というチャネルが導入されました。これは*encodeState
型のポインタを保持するバッファ付きチャネル (make(chan *encodeState, 8)
) です。このチャネルがオブジェクトプールとして機能します。newEncodeState()
関数: プールからencodeState
オブジェクトを取得します。select
ステートメントを使用して、まずチャネルからオブジェクトを取得しようとします。- 取得できた場合は、そのオブジェクトの
Reset()
メソッドを呼び出して状態を初期化し、返します。 - チャネルが空の場合は、新しく
encodeState
オブジェクトを割り当てて返します。
putEncodeState(e *encodeState)
関数: 使用済みのencodeState
オブジェクトをプールに返却します。select
ステートメントを使用して、チャネルにオブジェクトを送信しようとします。- チャネルが満杯の場合は、オブジェクトは破棄され、ガベージコレクタによって回収されます。これにより、プールのサイズが過度に大きくなるのを防ぎます。
-
Encoder.Encode
メソッドの変更:src/pkg/encoding/json/stream.go
のEncoder.Encode
メソッドが変更されました。- 以前は
enc.e.Reset()
を呼び出してEncoder
内部のencodeState
をリセットしていました。 - 変更後は、
e := newEncodeState()
を呼び出してプールからencodeState
を取得し、そのe
を使ってエンコーディング処理を行います。 - 処理の最後に
putEncodeState(e)
を呼び出して、使用済みのencodeState
をプールに返却します。
- 以前は
この変更により、Encoder
が頻繁に生成される場合でも、encodeState
の新規割り当てが大幅に削減され、既存のオブジェクトが再利用されるようになります。これにより、ガベージコレクションの頻度と負荷が低減され、特に高スループットな環境でのパフォーマンスが向上します。
ベンチマーク結果が示すように、操作あたりのバイト割り当てが 283
バイトから 102
バイトへと約64%削減されています。これは、encodeState
とその内部バッファの新規割り当てが減ったことによる直接的な効果です。操作あたりの時間(ns/op
)の改善はわずかですが、これはGCの負荷軽減が直接的な実行時間よりも、アプリケーション全体のレイテンシーの安定性やGC一時停止時間の短縮に寄与することを示唆しています。
コアとなるコードの変更箇所
src/pkg/encoding/json/encode.go
encodeStatePool
チャネルの追加:encodeState
オブジェクトをプールするためのバッファ付きチャネルが定義されました。newEncodeState()
関数の追加:encodeStatePool
からencodeState
オブジェクトを取得し、必要に応じて新規作成するロジックが追加されました。putEncodeState()
関数の追加: 使用済みのencodeState
オブジェクトをencodeStatePool
に返却するロジックが追加されました。
--- a/src/pkg/encoding/json/encode.go
+++ b/src/pkg/encoding/json/encode.go
@@ -227,6 +227,26 @@ type encodeState struct {
scratch [64]byte
}
+// 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:
+ }
+}
+
func (e *encodeState) marshal(v interface{}) (err error) {
defer func() {
if r := recover(); r != nil {
src/pkg/encoding/json/stream.go
Encoder.Encode
メソッド内で、encodeState
の取得と返却にnewEncodeState()
とputEncodeState()
が使用されるように変更されました。
--- a/src/pkg/encoding/json/stream.go
+++ b/src/pkg/encoding/json/stream.go
@@ -156,8 +156,8 @@ func (enc *Encoder) Encode(v interface{}) error {
if enc.err != nil {
return enc.err
}
- enc.e.Reset()
- err := enc.e.marshal(v)
+ e := newEncodeState()
+ err := e.marshal(v)
if err != nil {
return err
}
@@ -168,11 +168,12 @@ func (enc *Encoder) Encode(v interface{}) error {
// is required if the encoded value was a number,
// so that the reader knows there aren't more
// digits coming.
- enc.e.WriteByte('\n')
+ e.WriteByte('\n')
- if _, err = enc.w.Write(enc.e.Bytes()); err != nil {
+ if _, err = enc.w.Write(e.Bytes()); err != nil {
enc.err = err
}
+ putEncodeState(e)
return err
}
src/pkg/encoding/json/stream_test.go
BenchmarkEncoderEncode
という新しいベンチマークテストが追加されました。このテストは、NewEncoder
を繰り返し呼び出してEncode
メソッドを実行し、Encoder
の生成と使用におけるメモリ割り当てと実行時間を測定します。これにより、変更の効果を定量的に評価できるようになりました。
--- a/src/pkg/encoding/json/stream_test.go
+++ b/src/pkg/encoding/json/stream_test.go
@@ -191,3 +191,16 @@ func TestBlocking(t *testing.T) {
w.Close()
}
}
+
+func BenchmarkEncoderEncode(b *testing.B) {
+ b.ReportAllocs()
+ type T struct {
+ X, Y string
+ }
+ v := &T{"foo", "bar"}
+ for i := 0; i < b.N; i++ {
+ if err := NewEncoder(ioutil.Discard).Encode(v); err != nil {
+ b.Fatal(err)
+ }
+ }
+}
コアとなるコードの解説
このコミットの核心は、encodeState
オブジェクトのライフサイクルを Encoder
から切り離し、再利用可能なオブジェクトプールを導入した点にあります。
-
encodeStatePool
(チャネルベースのプール):var encodeStatePool = make(chan *encodeState, 8)
は、encodeState
型のポインタを最大8個まで保持できるバッファ付きチャネルを宣言しています。これは、Goの並行処理プリミティブであるチャネルをオブジェクトプールとして利用する典型的なパターンです。バッファサイズを8に設定することで、同時に8つのencodeState
オブジェクトをプールしておくことができ、それ以上のオブジェクトが必要になった場合は新規に割り当てられます。 -
newEncodeState()
: この関数は、encodeState
オブジェクトを取得する役割を担います。select
ステートメントを使用することで、ノンブロッキングでチャネルからの受信を試みます。case e := <-encodeStatePool:
: プールに利用可能なencodeState
オブジェクトがあれば、それを取り出し、e.Reset()
を呼び出して内部状態(特にbytes.Buffer
)を初期化してから返します。これにより、以前のエンコーディング処理で残ったデータが次の処理に影響を与えるのを防ぎます。default:
: プールが空の場合(チャネルからオブジェクトを取得できなかった場合)、return new(encodeState)
を実行して新しいencodeState
オブジェクトをヒープに割り当てて返します。
-
putEncodeState(e *encodeState)
: この関数は、使用済みのencodeState
オブジェクトをプールに返却する役割を担います。 ここでもselect
ステートメントが使用されます。case encodeStatePool <- e:
: プールに空きがあれば、e
をチャネルに送信してプールに戻します。default:
: プールが満杯の場合(チャネルにオブジェクトを送信できなかった場合)、default
ブロックが実行され、何もせずに関数を終了します。この場合、e
はチャネルに戻されず、参照がなくなるため、Goのガベージコレクタによって自動的に回収されます。これにより、プールのサイズが固定され、メモリ使用量が無制限に増加するのを防ぎます。
-
Encoder.Encode
メソッドの変更:Encoder.Encode
メソッドは、JSONエンコーディングのたびに呼び出される主要な関数です。- 以前は
enc.e.Reset()
を呼び出して、Encoder
構造体内に直接埋め込まれたencodeState
をリセットしていました。 - 変更後は、エンコーディング処理の開始時に
e := newEncodeState()
を呼び出してプールからencodeState
を取得します。 - エンコーディング処理中は、取得した
e
を使用してデータを書き込みます(例:e.marshal(v)
,e.WriteByte('\n')
,e.Bytes()
)。 - 処理の最後に
putEncodeState(e)
を呼び出して、encodeState
をプールに返却します。
- 以前は
この一連の変更により、Encoder
が繰り返し使用される場合でも、encodeState
オブジェクトの新規割り当てが大幅に削減されます。特に、NewEncoder
が頻繁に呼び出されるシナリオ(例: 各HTTPリクエストで新しい Encoder
を作成するWebサーバー)において、ガベージコレクションの負荷が軽減され、アプリケーションのメモリフットプリントとレイテンシーが改善されます。
関連リンク
- Go言語の
encoding/json
パッケージのドキュメント: https://pkg.go.dev/encoding/json - Go言語の
sync.Pool
パッケージのドキュメント: https://pkg.go.dev/sync#Pool (このコミット時点ではsync.Pool
は存在しないが、概念的に関連) - Go言語のガベージコレクションに関する記事 (例: Go's Garbage Collector): https://go.dev/blog/go15gc
参考にした情報源リンク
- コミットメッセージ内のGo CLリンク: https://golang.org/cl/9365044
- Go言語のメモリ管理とガベージコレクションに関する一般的な知識
- オブジェクトプールのデザインパターンに関する一般的な知識
- Go言語のチャネルの利用方法に関する一般的な知識
encoding/json
パッケージのソースコード (コミット前後の比較)bytes.Buffer
の利用方法に関する知識- Go言語のベンチマークの読み方に関する知識
- Brad Fitzpatrick氏のGo言語への貢献に関する情報 (Goの主要な開発者の一人)
ioutil.Discard
の利用方法に関する知識 (ベンチマークテストで使用)select
ステートメントのノンブロッキング利用に関する知識defer
とrecover
を用いたパニックハンドリングに関する知識 (既存コードの理解のため)io.Writer
インターフェースに関する知識 (Encoder
の出力先)interface{}
と型アサーションに関する知識 (Goの型システム)ReportAllocs()
ベンチマークオプションに関する知識I have generated the detailed technical explanation in Markdown format, as requested. I have included all the specified sections and filled them with information extracted from the commit data and general knowledge about Go'sencoding/json
package, garbage collection, and object pooling. I also added a new benchmark test explanation.
I will now output the generated Markdown content to standard output.
# [インデックス 16298] ファイルの概要
このコミットは、Go言語の標準ライブラリである `encoding/json` パッケージにおける `Encoder` のメモリ割り当てを削減することを目的としています。具体的には、JSONエンコーディング処理中に一時的に使用される `encodeState` オブジェクトの再利用メカニズムを導入することで、ガベージコレクションの負荷を軽減し、パフォーマンスを向上させています。
## コミット
commit f1583bb9563827fe132c97798657a6c60e6a0457 Author: Brad Fitzpatrick bradfitz@golang.org Date: Tue May 14 15:50:46 2013 -0700
encoding/json: allocate less in NewEncoder
The *Encoder is almost always garbage. It doesn't need an
encodeState inside of it (and its bytes.Buffer), since it's
only needed locally inside of Encode.
benchmark old ns/op new ns/op delta
BenchmarkEncoderEncode 2562 2553 -0.35%
benchmark old bytes new bytes delta
BenchmarkEncoderEncode 283 102 -63.96%
R=r
CC=gobot, golang-dev
https://golang.org/cl/9365044
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/f1583bb9563827fe132c97798657a6c60e6a0457](https://github.com/golang/go/commit/f1583bb9563827fe132c97798657a6c60e6a0457)
## 元コミット内容
`encoding/json` パッケージの `Encoder` が `encodeState` オブジェクトを内部に保持していたため、`Encoder` が生成されるたびに `encodeState` も一緒に割り当てられていました。しかし、`encodeState` は `Encode` メソッドの実行中にのみ必要とされる一時的なオブジェクトであり、`Encoder` 自体は頻繁に再利用されることが少ないため、`encodeState` の割り当てが不要なガベージを生成していました。このコミットは、`encodeState` を `Encoder` の内部から切り離し、必要に応じてプールから取得・返却するメカニズムを導入することで、メモリ割り当てを削減し、ガベージコレクションのオーバーヘッドを低減することを目的としています。
ベンチマーク結果は以下の通りです。
- `BenchmarkEncoderEncode` の実行時間はわずかに改善(-0.35%)
- `BenchmarkEncoderEncode` のメモリ割り当ては大幅に削減(-63.96%)
## 変更の背景
Go言語では、ガベージコレクション(GC)が自動的にメモリ管理を行いますが、頻繁なオブジェクトの生成と破棄はGCの負荷を増大させ、アプリケーションのパフォーマンスに影響を与える可能性があります。特に、ループ内で繰り返し生成される一時的なオブジェクトは、GCの頻度を高め、レイテンシースパイクを引き起こす原因となります。
`encoding/json` パッケージの `Encoder` は、JSONエンコーディングを行う際に内部で `encodeState` という構造体を使用します。この `encodeState` は、エンコーディング処理の途中で必要なバッファ(`bytes.Buffer`)や一時的なスクラッチ領域を保持しています。コミットメッセージにあるように、これまでの実装では `*Encoder` が生成されるたびに `encodeState` も一緒に割り当てられていました。しかし、`*Encoder` 自体は一度作成されると複数の `Encode` 呼び出しで再利用されることがありますが、`encodeState` は各 `Encode` 呼び出しのたびにリセットされて使用される一時的なデータであり、`Encoder` のライフサイクル全体にわたって保持する必要はありませんでした。
この設計は、`Encoder` が頻繁に生成・破棄されるようなシナリオ(例: 各HTTPリクエストごとに新しい `Encoder` を作成する場合)において、`encodeState` とその内部バッファの不要なメモリ割り当てと解放を繰り返し発生させ、ガベージコレクションの負担を増やしていました。このコミットは、この非効率性を解消し、メモリ割り当てを削減することで、アプリケーション全体のパフォーマンス、特にレイテンシーの安定性向上を目指しています。
## 前提知識の解説
### Go言語のガベージコレクション (GC)
Go言語は、自動メモリ管理のためにトレース型ガベージコレクタを採用しています。開発者は明示的にメモリを解放する必要がなく、GCが不要になったオブジェクトを自動的に回収します。しかし、GCは完全にオーバーヘッドがないわけではありません。GCが動作する際には、アプリケーションの実行が一時的に停止(ストップ・ザ・ワールド)したり、CPUリソースを消費したりすることがあります。特に、ヒープ上に大量のオブジェクトが頻繁に割り当てられると、GCの頻度が増加し、アプリケーションのパフォーマンスに悪影響を与える可能性があります。
### オブジェクトプール (Object Pool)
オブジェクトプールは、頻繁に生成・破棄されるオブジェクトのメモリ割り当てと解放のオーバーヘッドを削減するためのデザインパターンです。オブジェクトを使い終わった後すぐに解放するのではなく、再利用可能なオブジェクトの「プール」に戻します。次に同じ種類のオブジェクトが必要になったときには、新しく割り当てる代わりにプールから既存のオブジェクトを取得して再利用します。これにより、GCの負荷を軽減し、パフォーマンスを向上させることができます。
Go言語では、`sync.Pool` パッケージが汎用的なオブジェクトプールを提供しています。これは、一時的なオブジェクトの再利用に非常に効果的です。このコミットでは、`sync.Pool` が導入される前のGoのバージョンであるため、カスタムのチャネルベースのプール (`encodeStatePool`) が実装されています。
### `encoding/json` パッケージの `Encoder` と `encodeState`
`encoding/json` パッケージは、Goのデータ構造とJSONデータの間で変換を行うための機能を提供します。
- `json.Encoder`: Goの値をJSON形式でストリームに書き込むための型です。`NewEncoder` 関数で作成され、`Encode` メソッドを呼び出すことで値のエンコーディングを行います。
- `encodeState`: `Encoder` の内部で使用されるヘルパー構造体です。JSONエンコーディングの過程で必要な状態(例: `bytes.Buffer` を使った出力バッファ、一時的なバイト配列 `scratch`)を保持します。これまでの実装では、`Encoder` のフィールドとして `encodeState` が含まれていました。
## 技術的詳細
このコミットの主要な変更点は、`encodeState` オブジェクトのライフサイクル管理を `Encoder` から分離し、オブジェクトプール(チャネルベースのカスタム実装)を導入したことです。
1. **`encodeState` の分離**:
以前は `json.Encoder` 構造体の中に `encodeState` のインスタンスが直接埋め込まれていました。
```go
type Encoder struct {
w io.Writer
err error
e encodeState // 以前はここにencodeStateがあった
}
```
この変更により、`Encoder` は `encodeState` を直接保持しなくなりました。代わりに、`Encode` メソッドが呼び出されるたびに、プールから `encodeState` を取得し、処理が完了したらプールに返却するようになりました。
2. **`encodeStatePool` の導入**:
`src/pkg/encoding/json/encode.go` に `encodeStatePool` というチャネルが導入されました。これは `*encodeState` 型のポインタを保持するバッファ付きチャネル (`make(chan *encodeState, 8)`) です。このチャネルがオブジェクトプールとして機能します。
- `newEncodeState()` 関数: プールから `encodeState` オブジェクトを取得します。
- `select` ステートメントを使用して、まずチャネルからオブジェクトを取得しようとします。
- 取得できた場合は、そのオブジェクトの `Reset()` メソッドを呼び出して状態を初期化し、返します。
- チャネルが空の場合は、新しく `encodeState` オブジェクトを割り当てて返します。
- `putEncodeState(e *encodeState)` 関数: 使用済みの `encodeState` オブジェクトをプールに返却します。
- `select` ステートメントを使用して、チャネルにオブジェクトを送信しようとします。
- チャネルが満杯の場合は、オブジェクトは破棄され、ガベージコレクタによって回収されます。これにより、プールのサイズが過度に大きくなるのを防ぎます。
3. **`Encoder.Encode` メソッドの変更**:
`src/pkg/encoding/json/stream.go` の `Encoder.Encode` メソッドが変更されました。
- 以前は `enc.e.Reset()` を呼び出して `Encoder` 内部の `encodeState` をリセットしていました。
- 変更後は、`e := newEncodeState()` を呼び出してプールから `encodeState` を取得し、その `e` を使ってエンコーディング処理を行います。
- 処理の最後に `putEncodeState(e)` を呼び出して、使用済みの `encodeState` をプールに返却します。
この変更により、`Encoder` が頻繁に生成される場合でも、`encodeState` の新規割り当てが大幅に削減され、既存のオブジェクトが再利用されるようになります。これにより、ガベージコレクションの頻度と負荷が低減され、特に高スループットな環境でのパフォーマンスが向上します。
ベンチマーク結果が示すように、操作あたりのバイト割り当てが `283` バイトから `102` バイトへと約64%削減されています。これは、`encodeState` とその内部バッファの新規割り当てが減ったことによる直接的な効果です。操作あたりの時間(`ns/op`)の改善はわずかですが、これはGCの負荷軽減が直接的な実行時間よりも、アプリケーション全体のレイテンシーの安定性やGC一時停止時間の短縮に寄与することを示唆しています。
## コアとなるコードの変更箇所
### `src/pkg/encoding/json/encode.go`
- `encodeStatePool` チャネルの追加: `encodeState` オブジェクトをプールするためのバッファ付きチャネルが定義されました。
- `newEncodeState()` 関数の追加: `encodeStatePool` から `encodeState` オブジェクトを取得し、必要に応じて新規作成するロジックが追加されました。
- `putEncodeState()` 関数の追加: 使用済みの `encodeState` オブジェクトを `encodeStatePool` に返却するロジックが追加されました。
```diff
--- a/src/pkg/encoding/json/encode.go
+++ b/src/pkg/encoding/json/encode.go
@@ -227,6 +227,26 @@ type encodeState struct {
scratch [64]byte
}
+// 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:
+ }
+}
+
func (e *encodeState) marshal(v interface{}) (err error) {
defer func() {
if r := recover(); r != nil {
src/pkg/encoding/json/stream.go
Encoder.Encode
メソッド内で、encodeState
の取得と返却にnewEncodeState()
とputEncodeState()
が使用されるように変更されました。
--- a/src/pkg/encoding/json/stream.go
+++ b/src/pkg/encoding/json/stream.go
@@ -156,8 +156,8 @@ func (enc *Encoder) Encode(v interface{}) error {
if enc.err != nil {
return enc.err
}
- enc.e.Reset()
- err := enc.e.marshal(v)
+ e := newEncodeState()
+ err := e.marshal(v)
if err != nil {
return err
}
@@ -168,11 +168,12 @@ func (enc *Encoder) Encode(v interface{}) error {
// is required if the encoded value was a number,
// so that the reader knows there aren't more
// digits coming.
- enc.e.WriteByte('\n')
+ e.WriteByte('\n')
- if _, err = enc.w.Write(enc.e.Bytes()); err != nil {
+ if _, err = enc.w.Write(e.Bytes()); err != nil {
enc.err = err
}
+ putEncodeState(e)
return err
}
src/pkg/encoding/json/stream_test.go
BenchmarkEncoderEncode
という新しいベンチマークテストが追加されました。このテストは、NewEncoder
を繰り返し呼び出してEncode
メソッドを実行し、Encoder
の生成と使用におけるメモリ割り当てと実行時間を測定します。これにより、変更の効果を定量的に評価できるようになりました。
--- a/src/pkg/encoding/json/stream_test.go
+++ b/src/pkg/encoding/json/stream_test.go
@@ -191,3 +191,16 @@ func TestBlocking(t *testing.T) {
w.Close()
}
}
+
+func BenchmarkEncoderEncode(b *testing.B) {
+ b.ReportAllocs()
+ type T struct {
+ X, Y string
+ }
+ v := &T{"foo", "bar"}
+ for i := 0; i < b.N; i++ {
+ if err := NewEncoder(ioutil.Discard).Encode(v); err != nil {
+ b.Fatal(err)
+ }
+ }
+}
コアとなるコードの解説
このコミットの核心は、encodeState
オブジェクトのライフサイクルを Encoder
から切り離し、再利用可能なオブジェクトプールを導入した点にあります。
-
encodeStatePool
(チャネルベースのプール):var encodeStatePool = make(chan *encodeState, 8)
は、encodeState
型のポインタを最大8個まで保持できるバッファ付きチャネルを宣言しています。これは、Goの並行処理プリミティブであるチャネルをオブジェクトプールとして利用する典型的なパターンです。バッファサイズを8に設定することで、同時に8つのencodeState
オブジェクトをプールしておくことができ、それ以上のオブジェクトが必要になった場合は新規に割り当てられます。 -
newEncodeState()
: この関数は、encodeState
オブジェクトを取得する役割を担います。select
ステートメントを使用することで、ノンブロッキングでチャネルからの受信を試みます。case e := <-encodeStatePool:
: プールに利用可能なencodeState
オブジェクトがあれば、それを取り出し、e.Reset()
を呼び出して内部状態(特にbytes.Buffer
)を初期化してから返します。これにより、以前のエンコーディング処理で残ったデータが次の処理に影響を与えるのを防ぎます。default:
: プールが空の場合(チャネルからオブジェクトを取得できなかった場合)、return new(encodeState)
を実行して新しいencodeState
オブジェクトをヒープに割り当てて返します。
-
putEncodeState(e *encodeState)
: この関数は、使用済みのencodeState
オブジェクトをプールに返却する役割を担います。 ここでもselect
ステートメントが使用されます。case encodeStatePool <- e:
: プールに空きがあれば、e
をチャネルに送信してプールに戻します。default:
: プールが満杯の場合(チャネルにオブジェクトを送信できなかった場合)、default
ブロックが実行され、何もせずに関数を終了します。この場合、e
はチャネルに戻されず、参照がなくなるため、Goのガベージコレクタによって自動的に回収されます。これにより、プールのサイズが固定され、メモリ使用量が無制限に増加するのを防ぎます。
-
Encoder.Encode
メソッドの変更:Encoder.Encode
メソッドは、JSONエンコーディングのたびに呼び出される主要な関数です。- 以前は
enc.e.Reset()
を呼び出して、Encoder
構造体内に直接埋め込まれたencodeState
をリセットしていました。 - 変更後は、エンコーディング処理の開始時に
e := newEncodeState()
を呼び出してプールからencodeState
を取得します。 - エンコーディング処理中は、取得した
e
を使用してデータを書き込みます(例:e.marshal(v)
,e.WriteByte('\n')
,e.Bytes()
)。 - 処理の最後に
putEncodeState(e)
を呼び出して、encodeState
をプールに返却します。
- 以前は
この一連の変更により、Encoder
が繰り返し使用される場合でも、encodeState
オブジェクトの新規割り当てが大幅に削減されます。特に、NewEncoder
が頻繁に呼び出されるシナリオ(例: 各HTTPリクエストで新しい Encoder
を作成するWebサーバー)において、ガベージコレクションの負荷が軽減され、アプリケーションのメモリフットプリントとレイテンシーが改善されます。
関連リンク
- Go言語の
encoding/json
パッケージのドキュメント: https://pkg.go.dev/encoding/json - Go言語の
sync.Pool
パッケージのドキュメント: https://pkg.go.dev/sync#Pool (このコミット時点ではsync.Pool
は存在しないが、概念的に関連) - Go言語のガベージコレクションに関する記事 (例: Go's Garbage Collector): https://go.dev/blog/go15gc
参考にした情報源リンク
- コミットメッセージ内のGo CLリンク: https://golang.org/cl/9365044
- Go言語のメモリ管理とガベージコレクションに関する一般的な知識
- オブジェクトプールのデザインパターンに関する一般的な知識
- Go言語のチャネルの利用方法に関する一般的な知識
encoding/json
パッケージのソースコード (コミット前後の比較)bytes.Buffer
の利用方法に関する知識- Go言語のベンチマークの読み方に関する知識
- Brad Fitzpatrick氏のGo言語への貢献に関する情報 (Goの主要な開発者の一人)
ioutil.Discard
の利用方法に関する知識 (ベンチマークテストで使用)select
ステートメントのノンブロッキング利用に関する知識defer
とrecover
を用いたパニックハンドリングに関する知識 (既存コードの理解のため)io.Writer
インターフェースに関する知識 (Encoder
の出力先)interface{}
と型アサーションに関する知識 (Goの型システム)ReportAllocs()
ベンチマークオプションに関する知識