[インデックス 14854] ファイルの概要
このコミットは、Go言語の標準ライブラリである encoding/json
パッケージにおける Unmarshal
関数のパフォーマンス改善を目的としています。特に、JSONのプリミティブ型(数値、文字列、真偽値)のデコード処理において、不要なスキャンおよびパースロジックをスキップすることで、大幅な高速化を実現しています。
コミット
encoding/json
パッケージの Unmarshal
関数において、プリミティブ型(非オブジェクト/非配列)のJSON値をデコードする際のパフォーマンスを向上させました。これにより、ベンチマークでは最大で約65%の高速化が確認されています。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/60abc6b577895e88f8e029772ff27f9e6917d23b
元コミット内容
commit 60abc6b577895e88f8e029772ff27f9e6917d23b
Author: Rick Arnold <rickarnoldjr@gmail.com>
Date: Thu Jan 10 17:58:45 2013 -0800
encoding/json: improve performance of Unmarshal on primitive types
Skip most of the scanning and parsing logic for simple (non-object/array) JSON values.
benchmark old ns/op new ns/op delta
BenchmarkUnmarshalInt 948 436 -54.01%
BenchmarkUnmarshalUint 930 427 -54.09%
BenchmarkUnmarshalString 1407 715 -49.18%
BenchmarkUnmarshalFloat 1114 536 -51.89%
BenchmarkUnmarshalBool 759 266 -64.95%
BenchmarkUnmarshalStruct 8165 8181 +0.20%
No significant effects on the go1 benchmarks:
benchmark old ns/op new ns/op delta
BenchmarkBinaryTree17 9647362752 9596196417 -0.53%
BenchmarkFannkuch11 5623613048 5518694872 -1.87%
BenchmarkGobDecode 32944041 33165434 +0.67%
BenchmarkGobEncode 21237482 21080554 -0.74%
BenchmarkGzip 750955920 749861980 -0.15%
BenchmarkGunzip 197369742 197886192 +0.26%
BenchmarkJSONEncode 79274091 78891137 -0.48%
BenchmarkJSONDecode 180257802 175280358 -2.76%
BenchmarkMandelbrot200 7396666 7388266 -0.11%
BenchmarkParse 11446460 11386550 -0.52%
BenchmarkRevcomp 1605152523 1599512029 -0.35%
BenchmarkTemplate 204538247 207765574 +1.58%
benchmark old MB/s new MB/s speedup
BenchmarkGobDecode 23.30 23.14 0.99x
BenchmarkGobEncode 36.14 36.41 1.01x
BenchmarkGzip 25.84 25.88 1.00x
BenchmarkGunzip 98.32 98.06 1.00x
BenchmarkJSONEncode 24.48 24.60 1.00x
BenchmarkJSONDecode 10.76 11.07 1.03x
BenchmarkParse 5.06 5.09 1.01x
BenchmarkRevcomp 158.34 158.90 1.00x
BenchmarkTemplate 9.49 9.34 0.98x
Fixes #3949.
R=golang-dev, dave, bradfitz, timo
CC=golang-dev
https://golang.org/cl/7068043
変更の背景
Go言語の encoding/json
パッケージは、JSONデータのエンコード(Goのデータ構造からJSONへ)およびデコード(JSONからGoのデータ構造へ)を提供する標準ライブラリです。Unmarshal
関数はJSONバイト列をGoのインターフェースにデコードする役割を担っています。
このコミットが行われた背景には、Unmarshal
関数がプリミティブ型(整数、浮動小数点数、文字列、真偽値)のJSON値を処理する際に、必要以上に複雑なスキャンおよびパースロジックを実行しているというパフォーマンス上の課題がありました。JSONのオブジェクト({...}
)や配列([...]
)は構造を持つため、その内部を再帰的に解析する必要がありますが、単一のプリミティブ値(例: 123
, "hello"
, true
)は構造を持たないため、より単純な方法で処理できるはずです。
既存の実装では、プリミティブ値であっても、オブジェクトや配列と同様の汎用的なパースパスを通っていたため、オーバーヘッドが生じていました。このコミットは、この非効率性を解消し、特にプリミティブ値のデコード性能を向上させることを目的としています。ベンチマーク結果が示すように、この変更はプリミティブ型に対して劇的なパフォーマンス改善をもたらしました。
前提知識の解説
JSON (JavaScript Object Notation)
JSONは、人間が読み書きしやすく、機械が解析しやすいデータ交換フォーマットです。主にウェブアプリケーションでデータを送受信するために使用されます。JSONは以下の6つのデータ型をサポートします。
- オブジェクト (Object): 波括弧
{}
で囲まれ、キーと値のペアの集合です。キーは文字列で、値は任意のJSONデータ型です。例:{"name": "Alice", "age": 30}
- 配列 (Array): 角括弧
[]
で囲まれ、順序付けられた値のリストです。値は任意のJSONデータ型です。例:[1, 2, 3]
- 文字列 (String): 二重引用符
""
で囲まれたUnicode文字のシーケンスです。例:"hello"
- 数値 (Number): 整数または浮動小数点数です。例:
123
,3.14
- 真偽値 (Boolean):
true
またはfalse
です。 - null: 空の値を示す
null
です。
このコミットで言及されている「プリミティブ型」とは、オブジェクトと配列以外の、文字列、数値、真偽値、nullを指します。
Go言語の encoding/json
パッケージ
Go言語の encoding/json
パッケージは、Goのデータ型とJSON形式の間で変換を行うための機能を提供します。
json.Marshal()
: Goのデータ構造をJSONバイト列にエンコードします。json.Unmarshal()
: JSONバイト列をGoのデータ構造にデコードします。
Unmarshal
関数は、JSONバイト列を解析し、指定されたGoのインターフェース(通常はポインタ)にその値を格納します。このプロセスには、JSONの構文解析(トークン化、ツリー構築など)と、Goの型への変換が含まれます。
JSONパースにおけるスキャンとパースロジック
JSONのパースは通常、以下のステップで行われます。
- 字句解析 (Lexical Analysis / Scanning): 入力バイト列をトークン(例:
{
,[
,"string"
,123
,true
など)に分割します。この段階では、空白文字のスキップなども行われます。 - 構文解析 (Syntactic Analysis / Parsing): トークンのストリームを文法規則に従って解析し、JSONの構造(オブジェクト、配列、プリミティブ)を認識します。この段階で、JSONの構造が正しいかどうかが検証されます。
従来の Unmarshal
実装では、プリミティブ値であっても、オブジェクトや配列を解析する際に使用される汎用的なスキャンおよびパースロジックが適用されていました。これは、JSONの先頭文字が {
または [
でない場合でも、内部的にはより複雑な状態遷移やトークンチェックが行われることを意味します。
Goのベンチマーク
Go言語には、コードのパフォーマンスを測定するための組み込みのベンチマーク機能があります。
ns/op
(nanoseconds per operation): 1回の操作にかかる平均ナノ秒。値が小さいほど高速です。MB/s
(megabytes per second): 1秒あたりに処理できるメガバイト数。値が大きいほど高速です。delta
: 変更前後のパフォーマンス変化率。負の値は改善、正の値は悪化を示します。speedup
: 変更前に対する変更後の速度向上倍率。1.00xより大きい値は高速化を示します。
コミットメッセージに記載されているベンチマーク結果は、このコミットが Unmarshal
の特定のケース(プリミティブ型)で大幅な高速化を実現したことを明確に示しています。一方で、go1 benchmarks
はGo言語全体の主要なベンチマークスイートであり、この変更がGo言語全体のパフォーマンスに大きな悪影響を与えていないことを示しています。
技術的詳細
このコミットの技術的な核心は、encoding/json
パッケージの Unmarshal
関数に、JSONバイト列の先頭をチェックする早期リターンパスを追加した点にあります。
従来の Unmarshal
関数は、入力されたJSONデータがオブジェクト({
で始まる)または配列([
で始まる)であるかどうかに関わらず、一般的なパースロジックを開始していました。しかし、JSONの仕様では、オブジェクトと配列以外のすべての値(文字列、数値、真偽値、null)は、{
や [
以外の文字で始まります。
この変更では、Unmarshal
関数の冒頭で、入力バイト列の最初の非空白文字をチェックします。
- 空白文字のスキップ: まず、入力バイト列の先頭から空白文字(スペース、タブ、改行、キャリッジリターン)をスキップします。これはJSONの柔軟なフォーマットに対応するためです。
- 先頭文字のチェック: スキップ後、最初の非空白文字が
{
または[
であるかどうかをチェックします。 - 早期パスの実行:
- もし最初の非空白文字が
{
でも[
でもない場合、それはプリミティブ型(文字列、数値、真偽値、null)であると判断できます。 - この場合、通常の複雑なスキャンおよびパースロジックをスキップし、
decodeState
構造体のliteralStore
メソッドを直接呼び出します。literalStore
は、プリミティブ値を効率的にデコードするための専用ロジックです。これにより、不要な状態遷移やトークン解析のオーバーヘッドが削減されます。 - この早期パスは、デコード対象がポインタ型であり、かつnilでない場合にのみ適用されます。これは、
Unmarshal
が値を格納するためにポインタを必要とするためです。
- もし最初の非空白文字が
- 通常パスの継続: 最初の非空白文字が
{
または[
であった場合、それはオブジェクトまたは配列であるため、従来の汎用的なパースロジック(new(decodeState).init(data)
以降の処理)が継続されます。
この最適化により、プリミティブ型のデコードにおいて、特に小さなJSON値の処理が大幅に高速化されました。ベンチマーク結果が示すように、UnmarshalInt
、UnmarshalUint
、UnmarshalString
、UnmarshalFloat
、UnmarshalBool
といったプリミティブ型のデコードが50%以上高速化されています。一方で、UnmarshalStruct
のようなオブジェクトのデコードにはほとんど影響がなく、Go言語全体のベンチマーク(go1 benchmarks
)にも悪影響がないことが確認されています。
コアとなるコードの変更箇所
src/pkg/encoding/json/decode.go
--- a/src/pkg/encoding/json/decode.go
+++ b/src/pkg/encoding/json/decode.go
@@ -52,6 +52,25 @@ import (
// an UnmarshalTypeError describing the earliest such error.
//
func Unmarshal(data []byte, v interface{}) error {
+
+ // skip heavy processing for primitive values
+ var first byte
+ var i int
+ for i, first = range data {
+ if !isSpace(rune(first)) {
+ break
+ }
+ }
+ if first != '{' && first != '[' {
+ rv := reflect.ValueOf(v)
+ if rv.Kind() != reflect.Ptr || rv.IsNil() {
+ return &InvalidUnmarshalError{reflect.TypeOf(v)}
+ }
+ var d decodeState
+ d.literalStore(data[i:], rv.Elem(), false)
+ return d.savedError
+ }
+
d := new(decodeState).init(data)
// Quick check for well-formedness.
src/pkg/encoding/json/decode_test.go
--- a/src/pkg/encoding/json/decode_test.go
+++ b/src/pkg/encoding/json/decode_test.go
@@ -205,6 +205,13 @@ var unmarshalTests = []unmarshalTest{
{in: `{"k1":1,"k2":"s","k3":[1,2.0,3e-3],"k4":{"kk1":"s","kk2":2}}`, ptr: new(interface{}), out: ifaceNumAsFloat64},
{in: `{"k1":1,"k2":"s","k3":[1,2.0,3e-3],"k4":{"kk1":"s","kk2":2}}`, ptr: new(interface{}), out: ifaceNumAsNumber, useNumber: true},
+ // raw values with whitespace
+ {in: "\n true ", ptr: new(bool), out: true},
+ {in: "\t 1 ", ptr: new(int), out: 1},
+ {in: "\r 1.2 ", ptr: new(float64), out: 1.2},
+ {in: "\t -5 \n", ptr: new(int16), out: int16(-5)},
+ {in: "\t \"a\\u1234\" \n", ptr: new(string), out: "a\\u1234"},
+
// Z has a "-" tag.
{in: `{"Y": 1, "Z": 2}`, ptr: new(T), out: T{Y: 1}},
@@ -217,6 +224,16 @@ var unmarshalTests = []unmarshalTest{
{in: `[1, 2, 3+]`, err: &SyntaxError{"invalid character '+' after array element", 9}},
{in: `{"X":12x}`, err: &SyntaxError{"invalid character 'x' after object key:value pair", 8}, useNumber: true},
+ // raw value errors
+ {in: "\x01 42", err: &SyntaxError{"invalid character '\\x01' looking for beginning of value", 1}},
+ {in: " 42 \x01", err: &SyntaxError{"invalid character '\\x01' after top-level value", 5}},
+ {in: "\x01 true", err: &SyntaxError{"invalid character '\\x01' looking for beginning of value", 1}},
+ {in: " false \x01", err: &SyntaxError{"invalid character '\\x01' after top-level value", 8}},
+ {in: "\x01 1.2", err: &SyntaxError{"invalid character '\\x01' looking for beginning of value", 1}},
+ {in: " 3.4 \x01", err: &SyntaxError{"invalid character '\\x01' after top-level value", 6}},
+ {in: "\x01 \"string\"", err: &SyntaxError{"invalid character '\\x01' looking for beginning of value", 1}},
+ {in: " \"string\" \x01", err: &SyntaxError{"invalid character '\\x01' after top-level value", 11}},
+
// array tests
{in: `[1, 2, 3]`, ptr: new([3]int), out: [3]int{1, 2, 3}},
{in: `[1, 2, 3]`, ptr: new([1]int), out: [1]int{1}},
コアとなるコードの解説
src/pkg/encoding/json/decode.go
の変更
Unmarshal
関数の冒頭に以下のコードブロックが追加されました。
// skip heavy processing for primitive values
var first byte
var i int
for i, first = range data {
if !isSpace(rune(first)) {
break
}
}
if first != '{' && first != '[' {
rv := reflect.ValueOf(v)
if rv.Kind() != reflect.Ptr || rv.IsNil() {
return &InvalidUnmarshalError{reflect.TypeOf(v)}
}
var d decodeState
d.literalStore(data[i:], rv.Elem(), false)
return d.savedError
}
// skip heavy processing for primitive values
: このコメントは、このコードブロックの目的がプリミティブ値に対する重い処理をスキップすることであることを示しています。- 空白文字のスキップ:
このvar first byte var i int for i, first = range data { if !isSpace(rune(first)) { break } }
for
ループは、入力data
バイト列の先頭から、isSpace
関数(JSONの空白文字を判定するヘルパー関数)がfalse
を返す最初の文字(非空白文字)を見つけるまでインデックスi
と文字first
を進めます。これにより、JSON値の実際の開始位置を特定します。 - プリミティブ値の判定と早期リターン:
if first != '{' && first != '[' { rv := reflect.ValueOf(v) if rv.Kind() != reflect.Ptr || rv.IsNil() { return &InvalidUnmarshalError{reflect.TypeOf(v)} } var d decodeState d.literalStore(data[i:], rv.Elem(), false) return d.savedError }
if first != '{' && first != '['
: 最初の非空白文字が{
でも[
でもない場合、それはJSONオブジェクトでも配列でもないことを意味します。したがって、この値はプリミティブ型(文字列、数値、真偽値、null)であると判断されます。rv := reflect.ValueOf(v)
:v
はUnmarshal
の第二引数で、デコード結果を格納するGoのインターフェースです。reflect.ValueOf(v)
を使用して、v
のリフレクション値を取得します。if rv.Kind() != reflect.Ptr || rv.IsNil()
:Unmarshal
は値を格納するためにポインタを必要とします。このチェックは、v
がポインタ型でない場合、またはnilポインタである場合にInvalidUnmarshalError
を返してエラー処理を行います。var d decodeState
:decodeState
はJSONデコードの状態を管理する内部構造体です。d.literalStore(data[i:], rv.Elem(), false)
: ここが最適化の核心です。literalStore
メソッドは、プリミティブなJSONリテラル(文字列、数値、真偽値、null)を効率的にパースし、対応するGoの型に変換してrv.Elem()
(v
が指す実際の値)に格納します。data[i:]
は、空白文字をスキップした後のJSONバイト列の残りを渡します。最後のfalse
引数は、literalStore
がトップレベルのJSON値として処理されることを示唆している可能性があります。return d.savedError
:literalStore
の実行中にエラーが発生した場合、そのエラーがd.savedError
に格納され、それがUnmarshal
関数の戻り値として返されます。
この変更により、プリミティブ値のデコードパスが大幅に短縮され、不要な汎用パースロジックの実行が回避されるため、パフォーマンスが向上します。
src/pkg/encoding/json/decode_test.go
の変更
テストファイルには、新しい最適化パスが正しく機能することを確認するためのテストケースが追加されています。
-
// raw values with whitespace
:{in: "\n true ", ptr: new(bool), out: true}, {in: "\t 1 ", ptr: new(int), out: 1}, {in: "\r 1.2 ", ptr: new(float64), out: 1.2}, {in: "\t -5 \n", ptr: new(int16), out: int16(-5)}, {in: "\t \"a\\u1234\" \n", ptr: new(string), out: "a\\u1234"},
これらのテストケースは、JSON値の前後や内部に空白文字が含まれている場合でも、プリミティブ値(真偽値、整数、浮動小数点数、文字列)が正しくデコードされることを検証しています。これは、
decode.go
で追加された空白文字スキップロジックの正確性を保証するために重要です。 -
// raw value errors
:{in: "\x01 42", err: &SyntaxError{"invalid character '\\x01' looking for beginning of value", 1}}, {in: " 42 \x01", err: &SyntaxError{"invalid character '\\x01' after top-level value", 5}}, // ... (同様のエラーケースが続く)
これらのテストケースは、JSONの構文として不正な文字(例:
\x01
)がプリミティブ値の前後や内部に存在する場合に、SyntaxError
が正しく報告されることを検証しています。これは、早期リターンパスが不正な入力に対して堅牢であることを確認するために重要です。
これらのテストケースの追加は、新しい最適化が既存の機能に悪影響を与えず、かつ新しいエッジケース(特に空白文字の扱い)を適切に処理できることを保証します。
関連リンク
- Go CL 7068043: https://golang.org/cl/7068043
- Go Issue 3949: https://golang.org/issue/3949 (このコミットが修正したIssue)
参考にした情報源リンク
- Go言語の
encoding/json
パッケージのドキュメント - JSONの仕様 (RFC 8259 など)
- Go言語のベンチマークに関するドキュメント
- Go言語のリフレクションに関するドキュメント
- GitHubのコミットページ: https://github.com/golang/go/commit/60abc6b577895e88f8e029772ff27f9e6917d23b