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

[インデックス 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 パッケージの EncoderencodeState オブジェクトを内部に保持していたため、Encoder が生成されるたびに encodeState も一緒に割り当てられていました。しかし、encodeStateEncode メソッドの実行中にのみ必要とされる一時的なオブジェクトであり、Encoder 自体は頻繁に再利用されることが少ないため、encodeState の割り当てが不要なガベージを生成していました。このコミットは、encodeStateEncoder の内部から切り離し、必要に応じてプールから取得・返却するメカニズムを導入することで、メモリ割り当てを削減し、ガベージコレクションのオーバーヘッドを低減することを目的としています。

ベンチマーク結果は以下の通りです。

  • 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 パッケージの EncoderencodeState

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 のインスタンスが直接埋め込まれていました。

    type Encoder struct {
        w io.Writer
        err error
        e encodeState // 以前はここにencodeStateがあった
    }
    

    この変更により、EncoderencodeState を直接保持しなくなりました。代わりに、Encode メソッドが呼び出されるたびに、プールから encodeState を取得し、処理が完了したらプールに返却するようになりました。

  2. encodeStatePool の導入: src/pkg/encoding/json/encode.goencodeStatePool というチャネルが導入されました。これは *encodeState 型のポインタを保持するバッファ付きチャネル (make(chan *encodeState, 8)) です。このチャネルがオブジェクトプールとして機能します。

    • newEncodeState() 関数: プールから encodeState オブジェクトを取得します。
      • select ステートメントを使用して、まずチャネルからオブジェクトを取得しようとします。
      • 取得できた場合は、そのオブジェクトの Reset() メソッドを呼び出して状態を初期化し、返します。
      • チャネルが空の場合は、新しく encodeState オブジェクトを割り当てて返します。
    • putEncodeState(e *encodeState) 関数: 使用済みの encodeState オブジェクトをプールに返却します。
      • select ステートメントを使用して、チャネルにオブジェクトを送信しようとします。
      • チャネルが満杯の場合は、オブジェクトは破棄され、ガベージコレクタによって回収されます。これにより、プールのサイズが過度に大きくなるのを防ぎます。
  3. Encoder.Encode メソッドの変更: src/pkg/encoding/json/stream.goEncoder.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 から切り離し、再利用可能なオブジェクトプールを導入した点にあります。

  1. encodeStatePool (チャネルベースのプール): var encodeStatePool = make(chan *encodeState, 8) は、encodeState 型のポインタを最大8個まで保持できるバッファ付きチャネルを宣言しています。これは、Goの並行処理プリミティブであるチャネルをオブジェクトプールとして利用する典型的なパターンです。バッファサイズを8に設定することで、同時に8つの encodeState オブジェクトをプールしておくことができ、それ以上のオブジェクトが必要になった場合は新規に割り当てられます。

  2. newEncodeState(): この関数は、encodeState オブジェクトを取得する役割を担います。 select ステートメントを使用することで、ノンブロッキングでチャネルからの受信を試みます。

    • case e := <-encodeStatePool:: プールに利用可能な encodeState オブジェクトがあれば、それを取り出し、e.Reset() を呼び出して内部状態(特に bytes.Buffer)を初期化してから返します。これにより、以前のエンコーディング処理で残ったデータが次の処理に影響を与えるのを防ぎます。
    • default:: プールが空の場合(チャネルからオブジェクトを取得できなかった場合)、return new(encodeState) を実行して新しい encodeState オブジェクトをヒープに割り当てて返します。
  3. putEncodeState(e *encodeState): この関数は、使用済みの encodeState オブジェクトをプールに返却する役割を担います。 ここでも select ステートメントが使用されます。

    • case encodeStatePool <- e:: プールに空きがあれば、e をチャネルに送信してプールに戻します。
    • default:: プールが満杯の場合(チャネルにオブジェクトを送信できなかった場合)、default ブロックが実行され、何もせずに関数を終了します。この場合、e はチャネルに戻されず、参照がなくなるため、Goのガベージコレクタによって自動的に回収されます。これにより、プールのサイズが固定され、メモリ使用量が無制限に増加するのを防ぎます。
  4. 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 CLリンク: https://golang.org/cl/9365044
  • Go言語のメモリ管理とガベージコレクションに関する一般的な知識
  • オブジェクトプールのデザインパターンに関する一般的な知識
  • Go言語のチャネルの利用方法に関する一般的な知識
  • encoding/json パッケージのソースコード (コミット前後の比較)
  • bytes.Buffer の利用方法に関する知識
  • Go言語のベンチマークの読み方に関する知識
  • Brad Fitzpatrick氏のGo言語への貢献に関する情報 (Goの主要な開発者の一人)
  • ioutil.Discard の利用方法に関する知識 (ベンチマークテストで使用)
  • select ステートメントのノンブロッキング利用に関する知識
  • deferrecover を用いたパニックハンドリングに関する知識 (既存コードの理解のため)
  • 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's encoding/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 から切り離し、再利用可能なオブジェクトプールを導入した点にあります。

  1. encodeStatePool (チャネルベースのプール): var encodeStatePool = make(chan *encodeState, 8) は、encodeState 型のポインタを最大8個まで保持できるバッファ付きチャネルを宣言しています。これは、Goの並行処理プリミティブであるチャネルをオブジェクトプールとして利用する典型的なパターンです。バッファサイズを8に設定することで、同時に8つの encodeState オブジェクトをプールしておくことができ、それ以上のオブジェクトが必要になった場合は新規に割り当てられます。

  2. newEncodeState(): この関数は、encodeState オブジェクトを取得する役割を担います。 select ステートメントを使用することで、ノンブロッキングでチャネルからの受信を試みます。

    • case e := <-encodeStatePool:: プールに利用可能な encodeState オブジェクトがあれば、それを取り出し、e.Reset() を呼び出して内部状態(特に bytes.Buffer)を初期化してから返します。これにより、以前のエンコーディング処理で残ったデータが次の処理に影響を与えるのを防ぎます。
    • default:: プールが空の場合(チャネルからオブジェクトを取得できなかった場合)、return new(encodeState) を実行して新しい encodeState オブジェクトをヒープに割り当てて返します。
  3. putEncodeState(e *encodeState): この関数は、使用済みの encodeState オブジェクトをプールに返却する役割を担います。 ここでも select ステートメントが使用されます。

    • case encodeStatePool <- e:: プールに空きがあれば、e をチャネルに送信してプールに戻します。
    • default:: プールが満杯の場合(チャネルにオブジェクトを送信できなかった場合)、default ブロックが実行され、何もせずに関数を終了します。この場合、e はチャネルに戻されず、参照がなくなるため、Goのガベージコレクタによって自動的に回収されます。これにより、プールのサイズが固定され、メモリ使用量が無制限に増加するのを防ぎます。
  4. 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 CLリンク: https://golang.org/cl/9365044
  • Go言語のメモリ管理とガベージコレクションに関する一般的な知識
  • オブジェクトプールのデザインパターンに関する一般的な知識
  • Go言語のチャネルの利用方法に関する一般的な知識
  • encoding/json パッケージのソースコード (コミット前後の比較)
  • bytes.Buffer の利用方法に関する知識
  • Go言語のベンチマークの読み方に関する知識
  • Brad Fitzpatrick氏のGo言語への貢献に関する情報 (Goの主要な開発者の一人)
  • ioutil.Discard の利用方法に関する知識 (ベンチマークテストで使用)
  • select ステートメントのノンブロッキング利用に関する知識
  • deferrecover を用いたパニックハンドリングに関する知識 (既存コードの理解のため)
  • io.Writer インターフェースに関する知識 (Encoder の出力先)
  • interface{} と型アサーションに関する知識 (Goの型システム)
  • ReportAllocs() ベンチマークオプションに関する知識