[インデックス 10474] ファイルの概要
このコミットは、Go言語の標準ライブラリ encoding/json パッケージにおけるJSONエンコーディングのパフォーマンス改善を目的としています。具体的には、Goの reflect パッケージを用いた構造体のフィールド情報の取得と解析処理をキャッシュすることで、エンコーディング速度を向上させています。
コミット
commit 6c9f466273e3214cce22bf4a94e662a3872b13ee
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Mon Nov 21 07:49:14 2011 -0800
json: speed up encoding, caching reflect calls
Before
json.BenchmarkCodeEncoder 10 181232100 ns/op 10.71 MB/s
json.BenchmarkCodeMarshal 10 184578000 ns/op 10.51 MB/s
After:
json.BenchmarkCodeEncoder 10 146444000 ns/op 13.25 MB/s
json.BenchmarkCodeMarshal 10 151428500 ns/op 12.81 MB/s
R=rsc, r
CC=golang-dev
https://golang.org/cl/5416046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6c9f466273e3214cce22bf4a94e662a3872b13ee
元コミット内容
json: speed up encoding, caching reflect calls
Before
json.BenchmarkCodeEncoder 10 181232100 ns/op 10.71 MB/s
json.BenchmarkCodeMarshal 10 184578000 ns/op 10.51 MB/s
After:
json.BenchmarkCodeEncoder 10 146444000 ns/op 13.25 MB/s
json.BenchmarkCodeMarshal 10 151428500 ns/op 12.81 MB/s
R=rsc, r
CC=golang-dev
https://golang.org/cl/5416046
変更の背景
Go言語の encoding/json パッケージは、Goのデータ構造とJSON形式の間で変換を行うための標準ライブラリです。JSONエンコーディングのプロセスでは、Goの構造体(struct)をJSONオブジェクトに変換する際に、構造体のフィールド情報(フィールド名、タグ、型など)を動的に取得する必要があります。この動的な情報取得にはGoの reflect パッケージが使用されます。
reflect パッケージは、実行時に型情報を検査・操作するための強力な機能を提供しますが、その操作は比較的コストが高いとされています。特に、JSONエンコーディングのように同じ型の構造体が繰り返しエンコードされるようなシナリオでは、毎回 reflect を使ってフィールド情報を取得・解析することはパフォーマンスのボトルネックとなり得ます。
このコミットが行われた2011年当時、Go言語はまだ比較的新しい言語であり、標準ライブラリのパフォーマンス最適化は活発に行われていました。JSONエンコーディングは多くのアプリケーションで頻繁に使用される機能であるため、その性能向上はGoアプリケーション全体のパフォーマンスに大きな影響を与えます。
このコミットの背景には、encoding/json パッケージが構造体のフィールド情報を繰り返し解析していることによるオーバーヘッドを特定し、そのオーバーヘッドを削減することでエンコーディング速度を向上させるという明確な目的がありました。コミットメッセージに示されているベンチマーク結果は、この最適化が実際に顕著なパフォーマンス改善をもたらしたことを裏付けています。
前提知識の解説
Go言語の reflect パッケージ
reflect パッケージは、Goプログラムが自身の構造を検査・操作するための機能を提供します。これにより、プログラムは実行時に変数の型、値、構造体のフィールドなどを動的に調べることができます。
reflect.Type: Goの型の情報を表します。構造体のフィールド名、タグ、メソッドなどのメタデータにアクセスできます。reflect.Value: Goの値の情報を表します。変数の実際の値にアクセスしたり、変更したりできます。v.Type():reflect.Valueからその型を表すreflect.Typeを取得します。t.Field(i):reflect.Typeが構造体の場合、指定されたインデックスiのフィールドのreflect.StructFieldを取得します。f.Tag.Get("json"): 構造体フィールドのタグから、指定されたキー(この場合は "json")に対応する値を取得します。JSONエンコーディングでは、json:"field_name,omitempty"のようなタグを使って、JSONでのフィールド名やエンコーディングオプションを指定します。
reflect パッケージは非常に強力ですが、その使用にはいくつかの注意点があります。
- パフォーマンスオーバーヘッド: リフレクションはコンパイル時に型が確定している通常の操作に比べて、実行時のオーバーヘッドが大きいです。これは、型情報を動的に解決し、メモリ上のデータ構造を操作するためです。
- 型安全性: リフレクションは型システムを迂回するため、誤った型操作を行うと実行時パニック(runtime panic)を引き起こす可能性があります。
JSONエンコーディングにおける構造体フィールドの処理
Goの encoding/json パッケージが構造体をJSONにエンコードする際、以下のステップで各フィールドを処理します。
- フィールドの列挙: 構造体のすべての公開フィールド(エクスポートされたフィールド、つまり名前が大文字で始まるフィールド)を列挙します。
- タグの解析: 各フィールドに付与された
jsonタグを解析します。json:"-": このフィールドはJSONに含めない。json:"custom_name": JSONでのフィールド名をcustom_nameにする。json:",omitempty": フィールドの値がゼロ値(数値の0、文字列の""、スライスのnilなど)の場合、JSONに含めない。json:",string": フィールドの値を文字列としてJSONにエンコードする(例: 数値を"123"のように)。
- 値の取得とエンコード: 各フィールドの実際の値を取得し、その型に応じてJSON形式に変換します。
このプロセスにおいて、フィールドの列挙とタグの解析は、構造体の型情報に依存します。同じ型の構造体が何度もエンコードされる場合、これらのステップは毎回同じ結果を生成するため、計算をキャッシュする余地があります。
キャッシュの概念
キャッシュとは、計算コストの高い処理の結果を一時的に保存しておき、同じ入力に対しては保存された結果を再利用することで、処理速度を向上させる技術です。
このコミットでは、構造体の型情報からJSONエンコーディングに必要なフィールド情報を抽出する処理がキャッシュの対象となります。一度解析した構造体の型については、そのフィールド情報をメモリに保存しておき、次回同じ型の構造体がエンコードされる際には、再解析することなくキャッシュされた情報を利用します。
sync.RWMutex
sync.RWMutex はGo言語の標準ライブラリ sync パッケージで提供される読み書きロック(Reader-Writer Mutex)です。
- 読み取りロック (RLock/RUnlock): 複数のゴルーチンが同時に読み取りアクセスすることを許可します。
- 書き込みロック (Lock/Unlock): 書き込みアクセス中は、他のすべての読み取りおよび書き込みアクセスをブロックします。
キャッシュのようなデータ構造では、複数のゴルーチンが同時に読み取りを行う可能性があるため、読み取りロックを使用することで並行性を高めることができます。書き込み(キャッシュへの追加や更新)は排他的に行う必要があります。
技術的詳細
このコミットの主要な技術的変更点は、JSONエンコーディング時に構造体のフィールド情報を動的に解析するのではなく、一度解析した情報をキャッシュするメカニズムを導入したことです。
-
encodeField構造体の導入:encodeFieldは、構造体の個々のフィールドに関するJSONエンコーディングに必要な情報をカプセル化するための新しい構造体です。i int: 構造体内のフィールドのインデックス。reflect.Value.Field(i)で実際の値にアクセスするために使用されます。tag string: JSONでのフィールド名(json:"name"タグで指定されたもの、またはデフォルトのフィールド名)。quoted bool:json:",string"オプションが指定されているかどうかのフラグ。omitEmpty bool:json:",omitempty"オプションが指定されているかどうかのフラグ。
-
encodeFieldsCacheマップの導入:map[reflect.Type][]encodeField型のグローバルマップencodeFieldsCacheが導入されました。- このマップは、
reflect.Type(構造体の型)をキーとして、その型に対応する[]encodeField(フィールド情報のスライス)を値として保持します。 - これにより、一度解析された構造体のフィールド情報は、その型をキーとしてキャッシュに保存され、再利用可能になります。
-
typeCacheLock(sync.RWMutex) の導入:encodeFieldsCacheは複数のゴルーチンからアクセスされる可能性があるため、並行アクセスから保護するためにsync.RWMutex型のtypeCacheLockが導入されました。- キャッシュからの読み取り時には
RLock()とRUnlock()を使用し、複数の読み取りを許可します。 - キャッシュへの書き込み(新しい型情報の追加)時には
Lock()とUnlock()を使用し、排他的アクセスを保証します。
-
encodeFields関数の実装:- この新しい関数は、与えられた
reflect.Type(構造体の型)に対して、そのフィールド情報を[]encodeFieldのスライスとして返します。 - 関数はまず
typeCacheLock.RLock()を使ってキャッシュからの読み取りを試みます。 - キャッシュに情報が存在すれば、それを返します。
- キャッシュに情報がなければ、
typeCacheLock.Lock()を取得し、構造体のフィールドをreflectを使って解析し、encodeFieldスライスを構築します。 - 構築したスライスを
encodeFieldsCacheに保存し、typeCacheLock.Unlock()を解放して結果を返します。 - 二重チェックロックパターン(double-checked locking pattern)が適用されており、
RLockでキャッシュミスした後にLockを取得した際にもう一度キャッシュを確認することで、複数のゴルーチンが同時にキャッシュミスしてフィールド解析を重複して行わないようにしています。
- この新しい関数は、与えられた
-
reflectValueQuotedメソッドの変更:encodeStateのreflectValueQuotedメソッド(構造体のエンコーディングを担当する部分)が変更されました。- 以前はループ内で
v.Type().Field(i)を呼び出し、毎回フィールド情報とタグを解析していました。 - 変更後は、
encodeFields(v.Type())を一度呼び出して、キャッシュされた(または新しく解析された)[]encodeFieldスライスを取得します。 - その後、このスライスをイテレートして、各フィールドのエンコーディングに必要な情報(
ef.i,ef.tag,ef.omitEmpty,ef.quoted)を直接利用します。これにより、ループ内での高コストなreflect呼び出しが削減されます。
この変更により、同じ型の構造体が複数回エンコードされる場合、2回目以降のエンコーディングではフィールド情報の解析コストが大幅に削減され、エンコーディング全体のパフォーマンスが向上します。
コアとなるコードの変更箇所
変更は src/pkg/encoding/json/encode.go ファイルに集中しています。
-
import文の追加:--- a/src/pkg/encoding/json/encode.go +++ b/src/pkg/encoding/json/encode.go @@ -16,6 +16,7 @@ import ( "runtime" "sort" "strconv" + "sync" "unicode" "unicode/utf8" )syncパッケージがインポートされています。 -
reflectValueQuotedメソッド内の構造体フィールド処理の変更:--- a/src/pkg/encoding/json/encode.go +++ b/src/pkg/encoding/json/encode.go @@ -295,28 +296,10 @@ func (e *encodeState) reflectValueQuoted(v reflect.Value, quoted bool) { case reflect.Struct: e.WriteByte('{') - t := v.Type() - n := v.NumField() first := true - for i := 0; i < n; i++ { - f := t.Field(i) - if f.PkgPath != "" { - continue - } - tag, omitEmpty, quoted := f.Name, false, false - if tv := f.Tag.Get("json"); tv != "" { - if tv == "-" { - continue - } - name, opts := parseTag(tv) - if isValidTag(name) { - tag = name - } - omitEmpty = opts.Contains("omitempty") - quoted = opts.Contains("string") - } - fieldValue := v.Field(i) - if omitEmpty && isEmptyValue(fieldValue) { + for _, ef := range encodeFields(v.Type()) { + fieldValue := v.Field(ef.i) + if ef.omitEmpty && isEmptyValue(fieldValue) { continue } if first { @@ -324,9 +307,9 @@ func (e *encodeState) reflectValueQuoted(v reflect.Value, quoted bool) { } else { e.WriteByte(',') } - e.string(tag) + e.string(ef.tag) e.WriteByte(':') - e.reflectValueQuoted(fieldValue, quoted) + e.reflectValueQuoted(fieldValue, ef.quoted) } e.WriteByte('}')reflect.Type().Field(i)を直接呼び出す代わりに、encodeFields(v.Type())から取得したencodeFieldスライスを使用するように変更されています。 -
新しい型
encodeFieldの定義:--- a/src/pkg/encoding/json/encode.go +++ b/src/pkg/encoding/json/encode.go @@ -470,3 +453,63 @@ func (e *encodeState) string(s string) (int, error) { e.WriteByte('"') return e.Len() - len0, nil } +\n+// encodeField contains information about how to encode a field of a +// struct. +type encodeField struct { +\ti int // field index in struct +\ttag string +\tquoted bool +\tomitEmpty bool +}\n ``` 構造体フィールドのエンコーディング情報を保持する `encodeField` 型が追加されています。 -
キャッシュとロック変数の定義:
--- a/src/pkg/encoding/json/encode.go +++ b/src/pkg/encoding/json/encode.go @@ -470,3 +453,63 @@ func (e *encodeState) string(s string) (int, error) { e.WriteByte('"') return e.Len() - len0, nil } +\n+// encodeField contains information about how to encode a field of a +// struct. +type encodeField struct {\n+\ti int // field index in struct\n+\ttag string\n+\tquoted bool\n+\tomitEmpty bool\n+}\n+\n+var (\n+\ttypeCacheLock sync.RWMutex\n+\tencodeFieldsCache = make(map[reflect.Type][]encodeField)\n+)\n ``` `typeCacheLock` と `encodeFieldsCache` が定義されています。 -
encodeFields関数の実装:--- a/src/pkg/encoding/json/encode.go +++ b/src/pkg/encoding/json/encode.go @@ -470,3 +453,63 @@ func (e *encodeState) string(s string) (int, error) { e.WriteByte('"') return e.Len() - len0, nil } +\n+// encodeField contains information about how to encode a field of a +// struct. +type encodeField struct {\n+\ti int // field index in struct\n+\ttag string\n+\tquoted bool\n+\tomitEmpty bool\n+}\n+\n+var (\n+\ttypeCacheLock sync.RWMutex\n+\tencodeFieldsCache = make(map[reflect.Type][]encodeField)\n+)\n+\n+// encodeFields returns a slice of encodeField for a given +// struct type. +func encodeFields(t reflect.Type) []encodeField {\n+\ttypeCacheLock.RLock()\n+\tfs, ok := encodeFieldsCache[t]\n+\ttypeCacheLock.RUnlock()\n+\tif ok {\n+\t\treturn fs\n+\t}\n+\n+\ttypeCacheLock.Lock()\n+\tdefer typeCacheLock.Unlock()\n+\tfs, ok = encodeFieldsCache[t]\n+\tif ok {\n+\t\treturn fs\n+\t}\n+\n+\tv := reflect.Zero(t)\n+\tn := v.NumField()\n+\tfor i := 0; i < n; i++ {\n+\t\tf := t.Field(i)\n+\t\tif f.PkgPath != "" {\n+\t\t\tcontinue\n+\t\t}\n+\t\tvar ef encodeField\n+\t\tef.i = i\n+\t\tef.tag = f.Name\n+\n+\t\ttv := f.Tag.Get("json")\n+\t\tif tv != "" {\n+\t\t\tif tv == "-" {\n+\t\t\t\tcontinue\t\t\t}\n+\t\t\tname, opts := parseTag(tv)\n+\t\t\tif isValidTag(name) {\n+\t\t\t\tef.tag = name\n+\t\t\t}\n+\t\t\tef.omitEmpty = opts.Contains("omitempty")\n+\t\t\tef.quoted = opts.Contains("string")\n+\t\t}\n+\t\tfs = append(fs, ef)\n+\t}\n+\tencodeFieldsCache[t] = fs\n+\treturn fs\n+}\n ``` 構造体のフィールド情報を解析し、キャッシュに保存・取得する `encodeFields` 関数が追加されています。
コアとなるコードの解説
このコミットの核心は、encodeFields 関数と、それによって導入されたキャッシュメカニズムです。
encodeFields 関数は、reflect.Type を引数に取り、その型に対応する []encodeField を返します。この関数は以下のロジックで動作します。
-
読み取りロックの取得とキャッシュの確認:
typeCacheLock.RLock() fs, ok := encodeFieldsCache[t] typeCacheLock.RUnlock() if ok { return fs }まず、
typeCacheLock.RLock()を取得して、encodeFieldsCacheから引数tに対応するフィールド情報fsを読み取ろうとします。もしキャッシュに存在すれば、ロックを解放してその情報をすぐに返します。これにより、複数のゴルーチンが同時にキャッシュを読み取ることができ、高い並行性が実現されます。 -
書き込みロックの取得と二重チェック:
typeCacheLock.Lock() defer typeCacheLock.Unlock() fs, ok = encodeFieldsCache[t] if ok { return fs }キャッシュに情報がなかった場合、
typeCacheLock.Lock()を取得します。これは排他ロックであり、他のすべての読み取りおよび書き込み操作をブロックします。ロックを取得した後、再度キャッシュを確認します(二重チェック)。これは、最初のRLockを解放してからLockを取得するまでの間に、別のゴルーチンがすでに同じ型情報をキャッシュに追加している可能性があるためです。もしこの時点で情報が見つかれば、それを返します。 -
フィールド情報の解析とキャッシュへの追加:
v := reflect.Zero(t) n := v.NumField() for i := 0; i < n; i++ { f := t.Field(i) if f.PkgPath != "" { continue // Unexported field } var ef encodeField ef.i = i ef.tag = f.Name tv := f.Tag.Get("json") if tv != "" { if tv == "-" { continue // Field explicitly ignored } name, opts := parseTag(tv) if isValidTag(name) { ef.tag = name } ef.omitEmpty = opts.Contains("omitempty") ef.quoted = opts.Contains("string") } fs = append(fs, ef) } encodeFieldsCache[t] = fs return fsまだキャッシュに情報がない場合、
reflect.Zero(t)を使ってその型のゼロ値のreflect.Valueを作成し、NumField()でフィールド数を取得します。その後、ループで各フィールドをイテレートし、t.Field(i)でreflect.StructFieldを取得します。f.PkgPath != ""のチェックは、フィールドがエクスポートされていない(非公開)場合をスキップするためです。GoのJSONエンコーダは公開フィールドのみを処理します。jsonタグの解析ロジックは、以前reflectValueQuotedメソッド内にあったものが、このencodeFields関数内に移動されました。parseTagやisValidTagといったヘルパー関数は既存のものです。- 解析されたフィールド情報(インデックス、タグ名、
omitempty、stringオプション)はencodeField構造体に格納され、fsスライスに追加されます。 - ループが完了したら、構築された
fsスライスをencodeFieldsCache[t]に保存します。これにより、次回同じ型の構造体がエンコードされる際には、この解析処理がスキップされ、キャッシュされた情報が直接利用されます。
このキャッシュ戦略により、encoding/json パッケージは、構造体の型情報からJSONエンコーディングに必要なメタデータを取得する際の reflect パッケージのオーバーヘッドを大幅に削減し、特に同じ型の構造体を繰り返しエンコードするシナリオでのパフォーマンスを向上させています。
関連リンク
- Go言語
encoding/jsonパッケージのドキュメント: https://pkg.go.dev/encoding/json - Go言語
reflectパッケージのドキュメント: https://pkg.go.dev/reflect - Go言語
syncパッケージのドキュメント: https://pkg.go.dev/sync - Go言語のコードレビューシステム (Gerrit): https://go-review.googlesource.com/ (このコミットのCL: https://golang.org/cl/5416046)
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード
- Go言語のベンチマークに関する一般的な知識
- キャッシュ戦略と並行プログラミングにおけるロックの概念
- Go言語の
reflectパッケージのパフォーマンス特性に関する一般的な情報