[インデックス 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はコミットメッセージには含まれていませんが、背景知識を補完するために参照される可能性があります。