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

[インデックス 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/jsonUnmarshal がプリミティブ型に対して遅いという具体的な問題が報告されていました。

前提知識の解説

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データをエンコード/デコードするための型です。Encoderio.Writer に、Decoderio.Reader からデータを扱います。

プリミティブ型と複合型(JSONの場合)

JSONにおけるデータ型は以下の通りです。

  • プリミティブ型:
    • 文字列 (string): ダブルクォーテーションで囲まれたUnicode文字のシーケンス。例: "hello"
    • 数値 (number): 整数または浮動小数点数。例: 123, 3.14
    • 真偽値 (boolean): true または false
    • null: null
  • 複合型:
    • オブジェクト (object): 波括弧 {} で囲まれた、キーと値のペアの順序なしのコレクション。キーは文字列でなければなりません。
    • 配列 (array): 角括弧 [] で囲まれた、値の順序付きリスト。

このコミットは、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デコード処理を効率的に行うために decodeStatescan といった構造体が使用されています。

  • decodeState: JSONデコード処理の状態を管理する内部構造体です。入力データ、現在の読み取り位置、エラー情報などを保持します。
  • scan: JSONの字句解析(スキャン)を行うための内部構造体です。入力バイトストリームを読み込み、JSONのトークン(文字列、数値、区切り文字など)を識別します。checkValid 関数はこの scan 構造体を利用してJSONの構文が正しいかを初期段階でチェックします。

通常、Unmarshal 関数が呼び出されると、decodeState が初期化され、JSONデータの構文チェックが行われた後、unmarshal メソッドが呼び出されて実際のデコード処理が開始されます。このコミットでは、プリミティブ型の場合にこの通常のフローの一部をスキップすることでパフォーマンスを向上させています。

技術的詳細

このコミットの主要な技術的詳細は、Unmarshal 関数がJSONデータの最初の非空白文字を検査し、それがオブジェクトの開始を示す { や配列の開始を示す [ でない場合に、プリミティブ型として特別に処理するロジックを追加した点にあります。

変更前の Unmarshal 関数は、まず new(decodeState).init(data) を呼び出してデコード状態を初期化し、その後 checkValid で構文チェックを行い、最後に d.unmarshal(v) で実際のデコード処理を行っていました。

変更後のフローは以下のようになります。

  1. checkValid による初期構文チェック: 変更前と同様に、まず checkValid(data, &d.scan) を呼び出してJSONデータの全体的な構文が正しいかをチェックします。これは、JSONデータがオブジェクトや配列であっても、プリミティブ型であっても、最初に構文エラーを検出するための重要なステップです。これにより、前回の試みで発生した「構文エラーを無視する」問題を回避しています。
  2. プリミティブ型判定の高速パス:
    • 入力データ 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 に保存されたエラー(もしあれば)を返して処理を終了します。
  3. 複合型(オブジェクト/配列)の通常パス: 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)であると判断されます。 この場合、以下の高速パスが実行されます。

  1. rv := reflect.ValueOf(v): デコード先のGoの変数 vreflect.Value に変換します。
  2. if rv.Kind() != reflect.Ptr || rv.IsNil(): v がポインタ型 (reflect.Ptr) でない、または nil ポインタである場合、InvalidUnmarshalError を返します。Unmarshal はデコード結果を格納するためにポインタを必要とするため、これは必須のチェックです。
  3. d.literalStore(data[i:], rv.Elem(), false): literalStoreencoding/json の内部関数で、JSONのプリミティブ値をGoの対応する型に直接デコードします。data[i:] は最初の非空白文字以降のJSONデータ、rv.Elem()v が指す実際の要素の reflect.Value を渡します。false はおそらく、値がトップレベルであるかどうかのフラグです。
  4. return d.savedError: literalStore の実行中に発生したエラー(例えば、型変換エラーなど)は d.savedError に格納されるため、それを返して関数を終了します。
	d.init(data)
	return d.unmarshal(v)
}

first{ または [ であった場合(つまり、JSONデータがオブジェクトまたは配列である場合)、通常のデコードフローに進みます。

  1. d.init(data): decodeState を入力データ data で完全に初期化します。これには、内部バッファやスキャナーの状態設定などが含まれます。
  2. return d.unmarshal(v): decodeStateunmarshal メソッドを呼び出し、オブジェクトや配列の複雑なデコードロジックを実行します。このメソッドは、再帰的に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のページです。詳細な議論や変更履歴を確認できます。

参考にした情報源リンク