[インデックス 15051] ファイルの概要
このコミットは、Go言語の標準ライブラリである encoding/json パッケージにおける Unmarshal 関数のパフォーマンス改善を目的としています。特に、JSONデータがプリミティブ型(文字列、数値、真偽値など)である場合のデコード処理を高速化することに焦点を当てています。これにより、JSONデコード全体のベンチマークにおいて顕著な速度向上が見られます。
コミット
- コミットハッシュ:
eea0f19990c2bcb2a5f92e60307428fc7e18e153 - Author: Rick Arnold rickarnoldjr@gmail.com
- Date: Wed Jan 30 17:53:48 2013 -0800
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/eea0f19990c2bcb2a5f92e60307428fc7e18e153
元コミット内容
encoding/json: improve performance of Unmarshal on primitive types
Attempt 2. The first fix was much faster but ignored syntax errors.
benchmark old ns/op new ns/op delta
BenchmarkCodeEncoder 74993543 72982390 -2.68%
BenchmarkCodeMarshal 77811181 75610637 -2.83%
BenchmarkCodeDecoder 213337123 190144982 -10.87%
BenchmarkCodeUnmarshal 212180972 190394852 -10.27%
BenchmarkCodeUnmarshalReuse 202113428 182106660 -9.90%
BenchmarkUnmarshalString 1343 919 -31.57%
BenchmarkUnmarshalFloat64 1149 908 -20.97%
BenchmarkUnmarshalInt64 967 778 -19.54%
BenchmarkSkipValue 28851581 28414125 -1.52%
benchmark old MB/s new MB/s speedup
BenchmarkCodeEncoder 25.88 26.59 1.03x
BenchmarkCodeMarshal 24.94 25.66 1.03x
BenchmarkCodeDecoder 9.10 10.21 1.12x
BenchmarkCodeUnmarshal 9.15 10.19 1.11x
BenchmarkSkipValue 69.05 70.11 1.02x
Fixes #3949.
R=rsc
CC=golang-dev
https://golang.org/cl/7231058
変更の背景
このコミットの背景には、Go言語の encoding/json パッケージにおける Unmarshal 関数のパフォーマンス課題がありました。特に、JSONデータがオブジェクトや配列ではなく、単一のプリミティブ型(例: "hello", 123, 3.14, true)である場合に、不必要な処理が実行され、効率が低下しているという問題が認識されていました。
元コミットメッセージにある「Attempt 2. The first fix was much faster but ignored syntax errors.」という記述から、以前にも同様のパフォーマンス改善が試みられたものの、その実装がJSONの構文エラーを適切に処理できないという問題(おそらく、エラーチェックをスキップしすぎたため)を抱えていたことが示唆されます。
このコミットは、その前回の試みの反省を踏まえ、構文エラーのチェックを維持しつつ、プリミティブ型のデコードを高速化する、より堅牢なアプローチを導入しています。ベンチマーク結果が示すように、特に BenchmarkUnmarshalString, BenchmarkUnmarshalFloat64, BenchmarkUnmarshalInt64 といったプリミティブ型に対する Unmarshal 処理で大幅な改善が見られ、全体的な Unmarshal および Decoder のパフォーマンス向上にも寄与しています。
この変更は、Go Issue #3949 に対応するものです。このIssueでは、encoding/json の Unmarshal がプリミティブ型に対して遅いという具体的な問題が報告されていました。
前提知識の解説
Go言語の encoding/json パッケージ
encoding/json パッケージは、Go言語でJSONデータをエンコード(Goのデータ構造からJSONへ)およびデコード(JSONからGoのデータ構造へ)するための標準ライブラリです。
json.Unmarshal(data []byte, v interface{}) error: この関数は、JSON形式のバイトスライスdataをGoの任意の型vにデコードします。vはポインタである必要があります。Unmarshalは、JSONデータとGoのデータ構造のフィールド名をマッチングさせ、適切な型変換を行います。json.Encoder/json.Decoder: ストリームベースでJSONデータをエンコード/デコードするための型です。Encoderはio.Writerに、Decoderはio.Readerからデータを扱います。
プリミティブ型と複合型(JSONの場合)
JSONにおけるデータ型は以下の通りです。
- プリミティブ型:
- 文字列 (string): ダブルクォーテーションで囲まれたUnicode文字のシーケンス。例:
"hello" - 数値 (number): 整数または浮動小数点数。例:
123,3.14 - 真偽値 (boolean):
trueまたはfalse。 - null:
null。
- 文字列 (string): ダブルクォーテーションで囲まれたUnicode文字のシーケンス。例:
- 複合型:
- オブジェクト (object): 波括弧
{}で囲まれた、キーと値のペアの順序なしのコレクション。キーは文字列でなければなりません。 - 配列 (array): 角括弧
[]で囲まれた、値の順序付きリスト。
- オブジェクト (object): 波括弧
このコミットは、JSONデータがプリミティブ型である場合に特化した最適化を行っています。
reflect パッケージ
Go言語の reflect パッケージは、実行時にプログラムの型情報を検査したり、値を操作したりするための機能を提供します。
reflect.ValueOf(v interface{}) Value: 任意のGoの値をreflect.Value型に変換します。これにより、その値の型や内容を検査・操作できるようになります。Value.Kind():reflect.Valueの基底型(Int,String,Struct,Ptrなど)を返します。Value.IsNil():reflect.Valueが表現する値がnilであるかどうかを返します。ポインタ、インターフェース、マップ、スライス、チャネル、関数に対して有効です。Value.Elem(): ポインタが指す要素のreflect.Valueを返します。例えば、*T型のreflect.Valueに対してElem()を呼び出すと、T型のreflect.Valueが得られます。InvalidUnmarshalError:json.Unmarshal関数が、デコード先のGoの型が不適切である場合に返すエラー型です。例えば、Unmarshalの第2引数にポインタではない値を渡した場合に発生します。
JSONデコードの内部構造 (decodeState, scan)
encoding/json パッケージの内部では、JSONデコード処理を効率的に行うために decodeState や scan といった構造体が使用されています。
decodeState: JSONデコード処理の状態を管理する内部構造体です。入力データ、現在の読み取り位置、エラー情報などを保持します。scan: JSONの字句解析(スキャン)を行うための内部構造体です。入力バイトストリームを読み込み、JSONのトークン(文字列、数値、区切り文字など)を識別します。checkValid関数はこのscan構造体を利用してJSONの構文が正しいかを初期段階でチェックします。
通常、Unmarshal 関数が呼び出されると、decodeState が初期化され、JSONデータの構文チェックが行われた後、unmarshal メソッドが呼び出されて実際のデコード処理が開始されます。このコミットでは、プリミティブ型の場合にこの通常のフローの一部をスキップすることでパフォーマンスを向上させています。
技術的詳細
このコミットの主要な技術的詳細は、Unmarshal 関数がJSONデータの最初の非空白文字を検査し、それがオブジェクトの開始を示す { や配列の開始を示す [ でない場合に、プリミティブ型として特別に処理するロジックを追加した点にあります。
変更前の Unmarshal 関数は、まず new(decodeState).init(data) を呼び出してデコード状態を初期化し、その後 checkValid で構文チェックを行い、最後に d.unmarshal(v) で実際のデコード処理を行っていました。
変更後のフローは以下のようになります。
checkValidによる初期構文チェック: 変更前と同様に、まずcheckValid(data, &d.scan)を呼び出してJSONデータの全体的な構文が正しいかをチェックします。これは、JSONデータがオブジェクトや配列であっても、プリミティブ型であっても、最初に構文エラーを検出するための重要なステップです。これにより、前回の試みで発生した「構文エラーを無視する」問題を回避しています。- プリミティブ型判定の高速パス:
- 入力データ
dataの先頭から空白文字をスキップし、最初の非空白文字firstを特定します。 - もし
firstが{(オブジェクトの開始) でも[(配列の開始) でもない場合、そのJSONデータはプリミティブ型(文字列、数値、真偽値、null)であると判断されます。 - この場合、デコード先のGoの型
vがポインタであり、かつnilでないことをreflectパッケージを使って確認します。vが*Tのようなポインタ型でない、またはnilポインタである場合は、InvalidUnmarshalErrorを返します。これはUnmarshalの一般的な要件です。 - 条件を満たした場合、
d.literalStore(data[i:], rv.Elem(), false)を直接呼び出します。literalStoreは、JSONのプリミティブ値をGoの対応する型に直接デコードする内部関数です。これにより、オブジェクトや配列の複雑なパースロジックをスキップし、大幅な高速化を実現します。 - デコードが完了したら、
d.savedErrorに保存されたエラー(もしあれば)を返して処理を終了します。
- 入力データ
- 複合型(オブジェクト/配列)の通常パス:
firstが{または[であった場合、JSONデータはオブジェクトまたは配列であると判断され、通常のデコードフローに進みます。d.init(data)を呼び出してdecodeStateを初期化します。d.unmarshal(v)を呼び出して、オブジェクトや配列の複雑なデコード処理を実行します。
この最適化により、プリミティブ型の場合には decodeState の完全な初期化や、unmarshal メソッド内の複雑なディスパッチロジックを回避できるようになり、特に小さなJSONデータ(例: 単一の文字列や数値)のデコードにおいて顕著なパフォーマンス向上が実現されました。ベンチマーク結果がその効果を明確に示しています。
コアとなるコードの変更箇所
src/pkg/encoding/json/decode.go ファイルの Unmarshal 関数が変更されています。
--- a/src/pkg/encoding/json/decode.go
+++ b/src/pkg/encoding/json/decode.go
@@ -52,16 +52,34 @@ import (
// an UnmarshalTypeError describing the earliest such error.
//
func Unmarshal(data []byte, v interface{}) error {
- d := new(decodeState).init(data)
+
+ // Quick check for well-formedness.
+ // Avoids filling out half a data structure
+ // before discovering a JSON syntax error.
+ var d decodeState
+ err := checkValid(data, &d.scan)
+ if err != nil {
+ return err
+ }
+
+ // skip heavy processing for primitive values
+ var first byte
+ var i int
+ for i, first = range data {
+ if first > ' ' || !isSpace(rune(first)) {
+ break
+ }
+ }
+ if first != '{' && first != '[' {
+ rv := reflect.ValueOf(v)
+ if rv.Kind() != reflect.Ptr || rv.IsNil() {
+ return &InvalidUnmarshalError{reflect.TypeOf(v)}
+ }
+ d.literalStore(data[i:], rv.Elem(), false)
+ return d.savedError
+ }
+
+ d.init(data)
+ return d.unmarshal(v)
+}
コアとなるコードの解説
変更された Unmarshal 関数の各部分を詳細に解説します。
func Unmarshal(data []byte, v interface{}) error {
Unmarshal 関数は、JSONバイトスライス data と、デコード結果を格納するGoのインターフェース v を引数にとります。v は通常、ポインタ型です。
// Quick check for well-formedness.
// Avoids filling out half a data structure
// before discovering a JSON syntax error.
var d decodeState
err := checkValid(data, &d.scan)
if err != nil {
return err
}
この部分は変更前も存在しましたが、decodeState の初期化方法が変わっています。
まず、decodeState 型の変数 d を宣言します。
次に、checkValid(data, &d.scan) を呼び出して、入力JSONデータ data の構文が正しいかを迅速にチェックします。d.scan はJSONの字句解析を行うための内部状態を保持します。このチェックは、JSONデータがオブジェクトや配列であっても、プリミティブ型であっても、最初に実行されます。これにより、不完全なデータ構造を構築する前に構文エラーを検出でき、リソースの無駄を省きます。エラーがあれば即座に返します。
// skip heavy processing for primitive values
var first byte
var i int
for i, first = range data {
if first > ' ' || !isSpace(rune(first)) {
break
}
}
ここからが、プリミティブ型に対する最適化の新しいロジックです。
data スライスを先頭からループし、最初の非空白文字 first を見つけます。isSpace は空白文字を判定する内部関数です。i はその非空白文字のインデックスを保持します。
if first != '{' && first != '[' {
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Ptr || rv.IsNil() {
return &InvalidUnmarshalError{reflect.TypeOf(v)}
}
d.literalStore(data[i:], rv.Elem(), false)
return d.savedError
}
first が { (JSONオブジェクトの開始) でも [ (JSON配列の開始) でもない場合、JSONデータはプリミティブ型(文字列、数値、真偽値、null)であると判断されます。
この場合、以下の高速パスが実行されます。
rv := reflect.ValueOf(v): デコード先のGoの変数vをreflect.Valueに変換します。if rv.Kind() != reflect.Ptr || rv.IsNil():vがポインタ型 (reflect.Ptr) でない、またはnilポインタである場合、InvalidUnmarshalErrorを返します。Unmarshalはデコード結果を格納するためにポインタを必要とするため、これは必須のチェックです。d.literalStore(data[i:], rv.Elem(), false):literalStoreはencoding/jsonの内部関数で、JSONのプリミティブ値をGoの対応する型に直接デコードします。data[i:]は最初の非空白文字以降のJSONデータ、rv.Elem()はvが指す実際の要素のreflect.Valueを渡します。falseはおそらく、値がトップレベルであるかどうかのフラグです。return d.savedError:literalStoreの実行中に発生したエラー(例えば、型変換エラーなど)はd.savedErrorに格納されるため、それを返して関数を終了します。
d.init(data)
return d.unmarshal(v)
}
first が { または [ であった場合(つまり、JSONデータがオブジェクトまたは配列である場合)、通常のデコードフローに進みます。
d.init(data):decodeStateを入力データdataで完全に初期化します。これには、内部バッファやスキャナーの状態設定などが含まれます。return d.unmarshal(v):decodeStateのunmarshalメソッドを呼び出し、オブジェクトや配列の複雑なデコードロジックを実行します。このメソッドは、再帰的にJSON構造を解析し、対応するGoのデータ構造にマッピングします。
この変更により、プリミティブ型の場合には d.init(data) や d.unmarshal(v) といった比較的重い処理をスキップし、直接 literalStore を呼び出すことで、デコード処理のオーバーヘッドを大幅に削減しています。
関連リンク
- Go Issue #3949:
encoding/json: Unmarshal is slow for primitive types- このコミットが修正したIssueです。具体的なIssueページへの直接リンクは元コミットメッセージにはありませんが、GoのIssueトラッカーで検索可能です。
- Go Code Review (Gerrit):
https://golang.org/cl/7231058- この変更がレビューされたGerritのページです。詳細な議論や変更履歴を確認できます。
参考にした情報源リンク
- Go言語の
encoding/jsonパッケージのドキュメント: https://pkg.go.dev/encoding/json - Go言語の
reflectパッケージのドキュメント: https://pkg.go.dev/reflect - JSON (JavaScript Object Notation) の公式ウェブサイト: https://www.json.org/json-ja.html
- Go言語のIssueトラッカー (Issue #3949の検索): https://github.com/golang/go/issues?q=is%3Aissue+3949
- Go言語のGerritコードレビューシステム: https://go.googlesource.com/go/+/refs/heads/master/src/encoding/json/decode.go (現在の
decode.goのソースコード) - Go言語のベンチマークに関する一般的な情報: https://go.dev/doc/articles/go_benchmarking.html
- Go言語の
encoding/jsonの内部実装に関するブログ記事や解説(一般的な知識として参照)- 例: "Go's JSON encoding/decoding" (GoのJSONエンコーディング/デコーディングに関する一般的な解説記事)
- 例: "Understanding Go's reflect package" (Goのreflectパッケージに関する一般的な解説記事)
- これらの具体的なURLはコミットメッセージには含まれていませんが、背景知識を補完するために参照される可能性があります。