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

[インデックス 10399] ファイルの概要

このコミットは、Go言語の標準ライブラリである encoding/json パッケージに、JSONデータのマーシャリング(Goのデータ構造からJSONへの変換)とアンマーシャリング(JSONからGoのデータ構造への変換)のパフォーマンスベンチマークを追加するものです。具体的には、大規模なJSONデータセットを用いたベンチマークテストが導入されています。

コミット

commit 552a556a400a5d8f6d2d233b442b00539a761cab
Author: Russ Cox <rsc@golang.org>
Date:   Tue Nov 15 10:58:19 2011 -0500

    encoding/json: add marshal/unmarshal benchmark

    R=bradfitz
    CC=golang-dev
    https://golang.org/cl/5387041

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/552a556a400a5d8f6d2d233b442b00539a761cab

元コミット内容

encoding/json: add marshal/unmarshal benchmark

R=bradfitz
CC=golang-dev
https://golang.org/cl/5387041

変更の背景

このコミットの背景には、Go言語の encoding/json パッケージのパフォーマンス最適化への継続的な取り組みがあります。JSONはWebアプリケーションやAPIで広く利用されるデータ交換フォーマットであり、その処理速度はアプリケーション全体のパフォーマンスに直結します。特に大規模なデータセットを扱う場合、マーシャリングとアンマーシャリングの効率は非常に重要になります。

このコミットが作成された2011年当時、Go言語はまだ比較的新しい言語であり、標準ライブラリの成熟度を高める段階にありました。encoding/json パッケージも例外ではなく、その性能特性を正確に把握し、将来的な改善の基盤を築くために、信頼性の高いベンチマークが必要とされていました。

ベンチマークを追加することで、以下のような目的が達成されます。

  1. 性能の現状把握: 現在の encoding/json パッケージのマーシャリング/アンマーシャリングの速度を客観的に測定できます。
  2. 回帰テスト: 将来の変更がパフォーマンスに悪影響を与えないか(性能劣化がないか)を自動的に検出できます。
  3. 最適化の指針: どの部分がボトルネックになっているかを特定し、性能改善のための具体的な指針を得ることができます。
  4. 比較基準: 他のJSONライブラリや異なる実装との性能比較を行う際の基準となります。

このコミットで追加されたベンチマークは、agl (Andrew Gerrand) のGo、WebKit、Chromiumプロジェクトにおける変更履歴のサマリーという、実際の(しかし匿名化された)大規模なJSONデータを使用しており、より現実的なシナリオでの性能評価を可能にしています。

前提知識の解説

1. JSON (JavaScript Object Notation)

JSONは、人間が読み書きしやすく、機械が解析しやすい軽量なデータ交換フォーマットです。JavaScriptのオブジェクトリテラルをベースにしており、キーと値のペアの集合(オブジェクト)や、値の順序付きリスト(配列)でデータを表現します。Web APIや設定ファイルなどで広く利用されています。

2. Go言語の encoding/json パッケージ

Go言語の標準ライブラリには、JSONデータのエンコード(マーシャリング)とデコード(アンマーシャリング)を行うための encoding/json パッケージが用意されています。

  • マーシャリング (Marshal): Goの構造体(struct)やその他のGoのデータ型をJSON形式のバイト列に変換する処理です。json.Marshal() 関数がこれを行います。
  • アンマーシャリング (Unmarshal): JSON形式のバイト列をGoの構造体やその他のGoのデータ型に変換する処理です。json.Unmarshal() 関数がこれを行います。

Goの構造体のフィールドには、json:"fieldname" のような構造体タグ(struct tag)を付与することで、JSONのキー名をカスタマイズしたり、フィールドを無視したりするなどの制御が可能です。

3. Go言語のベンチマークテスト

Go言語には、コードのパフォーマンスを測定するための組み込みのベンチマーク機能があります。testing パッケージの一部として提供されており、go test -bench=. コマンドで実行できます。

  • ベンチマーク関数: BenchmarkXxx(*testing.B) というシグネチャを持つ関数として定義されます。
  • *testing.B: ベンチマーク実行のためのコンテキストを提供します。
    • b.N: ベンチマーク関数が実行されるイテレーション回数。Goのテストフレームワークが自動的に調整し、統計的に有意な結果が得られるようにします。
    • b.StopTimer() / b.StartTimer(): 測定対象の処理の開始と停止を制御します。初期化処理など、測定に含めたくない部分がある場合に利用します。
    • b.SetBytes(int64(n)): 1回の操作で処理されるバイト数を設定します。これにより、結果が「操作あたりのバイト数」として表示され、スループットの評価に役立ちます。

4. gzip 圧縮

gzip は、ファイル圧縮によく使われるデータフォーマットおよびソフトウェアです。このコミットでは、ベンチマーク用のJSONデータが gzip で圧縮された形式 (.json.gz) で提供されており、テストコード内で解凍して利用しています。これにより、リポジトリのサイズを抑えつつ、大規模なデータセットを扱うことが可能になります。

技術的詳細

このコミットでは、encoding/json パッケージのベンチマークテストファイル bench_test.go が新規に追加されています。このファイルは、Goのベンチマークフレームワークを利用して、JSONのエンコード(Marshal/Encoder)とデコード(Unmarshal/Decoder)の性能を測定します。

データ構造の定義

ベンチマークに使用されるJSONデータに対応するGoの構造体が定義されています。

type codeResponse struct {
	Tree     *codeNode `json:"tree"`
	Username string    `json:"username"`
}

type codeNode struct {
	Name     string      `json:"name"`
	Kids     []*codeNode `json:"kids"`
	CLWeight float64     `json:"cl_weight"`
	Touches  int         `json:"touches"`
	MinT     int64       `json:"min_t"`
	MaxT     int64       `json:"max_t"`
	MeanT    int64       `json:"mean_t"`
}

codeResponse はルートとなる構造体で、Tree フィールドは codeNode 型のポインタ、Username は文字列です。codeNode は再帰的な構造を持ち、Kids フィールドが codeNode のスライスになっています。これは、ツリー構造のデータを表現するために用いられます。各フィールドには json:"..." タグが付与されており、JSONのキー名とGoの構造体フィールド名のマッピングを定義しています。

データ初期化 codeInit()

ベンチマーク実行前に一度だけ呼び出される codeInit() 関数が定義されています。この関数は以下の処理を行います。

  1. testdata/code.json.gz ファイルを開きます。
  2. gzip.NewReader を使用して、Gzip圧縮されたデータを読み込むためのリーダーを作成します。
  3. ioutil.ReadAll でGzipリーダーから全てのデータを読み込み、codeJSON グローバル変数に生JSONバイト列として格納します。
  4. codeJSONUnmarshal して codeStruct グローバル変数(Goの構造体)に変換します。これは、アンマーシャリングのベンチマークの準備と、後述の再マーシャリングチェックのために行われます。
  5. codeStruct を再度 Marshal して、元の codeJSON と比較します。これにより、マーシャリングとアンマーシャリングの往復でデータが破損しないことを確認しています。もしデータが異なれば panic します。このチェックは、ベンチマークの信頼性を保証するために重要です。

ベンチマーク関数

以下の5つのベンチマーク関数が定義されています。

  1. BenchmarkCodeEncoder(b *testing.B):

    • json.NewEncoder(ioutil.Discard) を使用して、出力先を ioutil.Discard (書き込まれたデータを破棄するWriter) に設定した json.Encoder を作成します。
    • enc.Encode(&codeStruct)b.N 回実行し、Goの構造体をJSONとしてエンコードする速度を測定します。
    • b.SetBytes(int64(len(codeJSON))) で、1回の操作で処理されるバイト数を設定し、スループット(バイト/秒)が計算されるようにします。
  2. BenchmarkCodeMarshal(b *testing.B):

    • json.Marshal(&codeStruct)b.N 回実行し、Goの構造体をJSONバイト列にマーシャリングする速度を測定します。
    • BenchmarkCodeEncoder と同様に b.SetBytes を設定します。
  3. BenchmarkCodeDecoder(b *testing.B):

    • bytes.Buffer を使用して、JSONデータをデコーダに供給します。
    • json.NewDecoder(&buf) を使用して json.Decoder を作成します。
    • ループ内で buf.Write(codeJSON) でJSONデータをバッファに書き込み、dec.Decode(&r) でデコードします。
    • buf.WriteByte('\n') を複数回呼び出すことで、json.Decoder がEOF (End Of File) を検出しないようにしています。これは、Decoder がストリームから連続してJSONオブジェクトを読み取るシナリオをシミュレートするためと考えられます。
    • b.SetBytes を設定します。
  4. BenchmarkCodeUnmarshal(b *testing.B):

    • json.Unmarshal(codeJSON, &r)b.N 回実行し、JSONバイト列をGoの構造体にアンマーシャリングする速度を測定します。
    • ループ内で毎回新しい codeResponse 型の変数 r を宣言しています。これは、アンマーシャリングのたびに新しいメモリが割り当てられるシナリオをシミュレートします。
    • b.SetBytes を設定します。
  5. BenchmarkCodeUnmarshalReuse(b *testing.B):

    • BenchmarkCodeUnmarshal と同様に json.Unmarshal を実行しますが、ループの外で一度だけ codeResponse 型の変数 r を宣言し、それを再利用します。
    • これにより、アンマーシャリングのたびに新しいメモリ割り当てが発生しないシナリオ(既存の構造体にデコードする)での性能を測定します。これは、メモリ再利用が可能な場合のパフォーマンス特性を評価するために重要です。
    • b.SetBytes を設定します。

テストデータ code.json.gz

src/pkg/encoding/json/testdata/code.json.gz は、ベンチマークに使用される実際のJSONデータを含むGzip圧縮ファイルです。このファイルはバイナリファイルとしてコミットされており、そのサイズは120432バイトです。このデータは、agl (Andrew Gerrand) のGo、WebKit、Chromiumプロジェクトにおける変更履歴のサマリーであり、現実的な大規模データセットを提供します。

コアとなるコードの変更箇所

このコミットで追加された主要なファイルは以下の2つです。

  1. src/pkg/encoding/json/bench_test.go (新規ファイル)

    • JSONのマーシャリングとアンマーシャリングのベンチマークテストコードが含まれています。
    • codeResponsecodeNode というGoの構造体が定義されています。
    • codeInit() 関数でベンチマークデータの読み込みと初期検証が行われます。
    • BenchmarkCodeEncoder, BenchmarkCodeMarshal, BenchmarkCodeDecoder, BenchmarkCodeUnmarshal, BenchmarkCodeUnmarshalReuse の5つのベンチマーク関数が実装されています。
  2. src/pkg/encoding/json/testdata/code.json.gz (新規バイナリファイル)

    • ベンチマークテストで使用される大規模なJSONデータがGzip圧縮された形式で含まれています。

コアとなるコードの解説

bench_test.go のコードは、Goのベンチマークテストの典型的なパターンに従っています。

  • パッケージ宣言: package json となっており、encoding/json パッケージの内部テストであることを示しています。
  • インポート: bytes, compress/gzip, io/ioutil, os, testing といった標準ライブラリがインポートされています。これらはファイル操作、Gzip解凍、バイト列操作、ベンチマーク実行のために必要です。
  • 構造体定義: codeResponsecodeNode は、ベンチマーク対象のJSONデータのスキーマをGoの型システムで表現したものです。json:"..." タグは、Goのフィールド名とJSONのキー名を対応させるために不可欠です。
  • codeInit(): この関数は、ベンチマークが開始される前に一度だけ実行され、必要なデータを準備します。
    • os.Opengzip.NewReader で圧縮ファイルを読み込みます。
    • ioutil.ReadAll で解凍されたJSONデータを codeJSON に格納します。
    • UnmarshalMarshal を連続して実行し、元のJSONデータと再マーシャリングされたデータが一致するかを検証します。これは、ベンチマークの入力データが正しく処理されることを保証するための重要な健全性チェックです。もし一致しない場合は、panic してテストを中断します。
  • ベンチマーク関数群: 各ベンチマーク関数は Benchmark プレフィックスを持ち、*testing.B 型の引数を取ります。
    • if codeJSON == nil { ... codeInit() ... }: 各ベンチマーク関数の冒頭で codeInit() が呼び出されます。codeJSON == nil のチェックにより、codeInit() はテストスイート全体で一度だけ実行されることが保証されます。b.StopTimer()b.StartTimer() は、初期化処理の時間をベンチマーク測定から除外するために使用されます。
    • for i := 0; i < b.N; i++ { ... }: このループ内で、測定対象の操作が b.N 回実行されます。b.N の値は、Goのテストフレームワークが自動的に調整し、統計的に有意な結果が得られるようにします。
    • b.SetBytes(int64(len(codeJSON))): この呼び出しにより、ベンチマーク結果に「操作あたりのバイト数」が表示されるようになり、スループットの評価が容易になります。

特に注目すべきは、BenchmarkCodeUnmarshalBenchmarkCodeUnmarshalReuse の違いです。前者はループ内で毎回新しい codeResponse インスタンスを生成してアンマーシャリングするのに対し、後者はループの外で一度だけインスタンスを生成し、それを再利用します。これにより、メモリ割り当てのオーバーヘッドがアンマーシャリングのパフォーマンスに与える影響を個別に測定できます。一般的に、既存のメモリを再利用できる場合は、パフォーマンスが向上する傾向があります。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のベンチマークに関するブログ記事やチュートリアル (一般的な知識として)
  • JSONデータフォーマットに関する一般的な情報 (一般的な知識として)
  • Gzip圧縮に関する一般的な情報 (一般的な知識として)