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

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

このコミットは、Go言語のencoding/jsonパッケージ内のベンチマークテストBenchmarkSkipValueの改善に関するものです。具体的には、ベンチマークの測定結果の一貫性を高め、ガベージコレクション(GC)による影響を最小限に抑えるために、scanner構造体の割り当てをループの外に移動しています。

コミット

commit a6106eef379ef560016d0dcdbdd9c9c86b7cd39c
Author: Russ Cox <rsc@golang.org>
Date:   Tue Nov 15 13:59:59 2011 -0500

    encoding/json: make BenchmarkSkipValue more consistent

    Move scanner allocation out of loop.
    It's the only allocation in the test so it dominates
    when it triggers a garbage collection.

    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/5369117

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

https://github.com/golang/go/commit/a6106eef379ef560016d0dcdbdd9c9c86b7cd39c

元コミット内容

encoding/json: make BenchmarkSkipValue more consistent

Move scanner allocation out of loop.
It's the only allocation in the test so it dominates
when it triggers a garbage collection.

変更の背景

この変更の背景には、Go言語のベンチマークテストにおける正確性と信頼性の向上が挙げられます。encoding/jsonパッケージのBenchmarkSkipValueは、JSONデータを解析せずに特定の値をスキップする処理のパフォーマンスを測定するためのものです。

元の実装では、scanner構造体のインスタンスがベンチマークループ内で毎回新しく割り当てられていました。Goのガベージコレクタ(GC)は、不要になったメモリを自動的に解放する役割を担いますが、この割り当てがループ内で頻繁に行われると、GCがトリガーされる頻度が増加し、ベンチマークの測定結果にばらつきが生じる原因となります。特に、このscannerの割り当てがテスト内で唯一の大きな割り当てであったため、GCの実行がベンチマーク結果に大きな影響を与えていました。

開発者は、BenchmarkSkipValueが純粋に値のスキップ処理の効率を測定することを意図しており、GCのオーバーヘッドがその測定を歪めることを避けたかったと考えられます。scannerの割り当てをループの外に移動することで、この割り当てがベンチマークの実行時間中に一度だけ行われるようになり、GCの介入が大幅に減少し、結果としてベンチマークの測定結果がより安定し、真のパフォーマンスを反映するようになります。

前提知識の解説

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

encoding/jsonパッケージは、GoプログラムでJSONデータをエンコード(Goのデータ構造からJSONへ)およびデコード(JSONからGoのデータ構造へ)するための標準ライブラリです。WebアプリケーションやAPI開発において、JSON形式のデータを扱う際に不可欠なパッケージです。

scanner

encoding/jsonパッケージの内部では、JSONデータを効率的に解析するためにscannerという内部型が使用されています。このscannerは、JSONストリームを読み込み、トークン化(JSONの要素を識別可能な単位に分解すること)を行う役割を担います。scannerは、JSONの構文解析状態を保持し、次のJSON要素を効率的に読み進めるための情報を含んでいます。

BenchmarkSkipValueベンチマーク

Go言語の標準ライブラリには、コードのパフォーマンスを測定するためのベンチマーク機能が組み込まれています。BenchmarkSkipValueは、encoding/jsonパッケージ内で定義されているベンチマーク関数の一つで、JSONデータの中から特定の値を「スキップ」する処理の速度を測定します。これは、JSON全体をデコードせずに、特定のフィールドだけを抽出したい場合などに役立つ機能です。例えば、非常に大きなJSONデータから一部の情報だけが必要な場合、不要な部分の解析をスキップすることで、処理時間を短縮し、メモリ使用量を削減できます。

Goのガベージコレクション(GC)

Go言語は、自動メモリ管理(ガベージコレクション)を採用しています。プログラマが手動でメモリを解放する必要がなく、Goランタイムが不要になったメモリを自動的に検出し、再利用可能な状態にします。GCは、プログラムの実行中にバックグラウンドで動作し、メモリの割り当てと解放のパターンに応じてトリガーされます。頻繁なメモリ割り当てはGCの実行頻度を高め、プログラムの実行に一時的な停止(ストップ・ザ・ワールド)を引き起こす可能性があり、特にベンチマークのような厳密なパフォーマンス測定においては、その影響が顕著に出ることがあります。

ベンチマークにおけるGCの影響

ベンチマークテストでは、測定対象のコードの純粋なパフォーマンスを評価することが重要です。しかし、テスト中に頻繁にメモリが割り当てられ、GCがトリガーされると、GCの実行時間がベンチマーク結果に加算され、測定対象のコード本来のパフォーマンスが正確に反映されなくなります。特に、ベンチマークループ内で毎回メモリ割り当てが行われるような場合、GCのオーバーヘッドが測定結果を支配してしまうことがあります。これを避けるためには、ベンチマークのセットアップ段階で必要なリソースを一度だけ割り当て、ループ内では可能な限りメモリ割り当てを避けることが推奨されます。

技術的詳細

このコミットの技術的詳細な変更点は、BenchmarkSkipValue関数内でのscanner構造体のインスタンス化のタイミングです。

変更前は、BenchmarkSkipValue関数のベンチマークループ(for i := 0; i < b.N; i++)の内部で、var scan scannerという行によってscanner構造体の新しいインスタンスが毎回宣言され、割り当てられていました。Goでは、構造体は値型であり、通常はスタックに割り当てられますが、そのサイズや使用方法によってはヒープにエスケープして割り当てられることがあります。ベンチマークループ内で毎回新しいscannerが作成されると、たとえそれがスタックに割り当てられたとしても、そのライフサイクルがループのイテレーションごとに完結し、不要なメモリ操作が発生します。もしヒープにエスケープしていた場合、これはGCの対象となり、GCの実行がベンチマークの測定結果に大きな影響を与えていました。

変更後のコードでは、var benchScan scannerというscanner構造体の変数が、BenchmarkSkipValue関数の外側、つまりパッケージレベルのグローバル変数として宣言されています。これにより、benchScanはベンチマークが開始される前に一度だけ割り当てられ、ベンチマークループの各イテレーションでは、この既存のbenchScanインスタンスが再利用されるようになります。

この変更によって得られる主な利点は以下の通りです。

  1. GCオーバーヘッドの削減: scannerの割り当てがループ外に移動したことで、ベンチマークループ内でメモリ割り当てが発生しなくなります。これにより、GCがトリガーされる頻度が大幅に減少し、GCによる一時停止がベンチマーク結果に与える影響が最小限に抑えられます。
  2. ベンチマークの一貫性向上: GCの介入が減ることで、ベンチマークの実行ごとに測定される時間がより安定し、一貫性のある結果が得られるようになります。これにより、SkipValue処理自体の純粋なパフォーマンスをより正確に評価できるようになります。
  3. リソース再利用の促進: このパターンは、Goのベンチマークでよく用いられる最適化手法の一つです。特に、sync.Poolのようなメカニズムが導入される前のGoのバージョンでは、このようにグローバル変数や関数スコープ外でリソースを一度だけ割り当てて再利用する手法が、パフォーマンスベンチマークの精度を高めるために重要でした。sync.Poolは、オブジェクトの再利用をより汎用的に行うためのGoの標準ライブラリ機能ですが、このコミットの時点(2011年)ではまだ存在していなかったか、あるいはこの特定のケースではシンプルなグローバル変数での再利用が適切と判断された可能性があります。

この変更は、Goのベンチマークを書く際のベストプラクティスを示しており、測定対象のコード以外の要因(この場合はメモリ割り当てとGC)がベンチマーク結果に影響を与えないようにするための典型的な例です。

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

--- a/src/pkg/encoding/json/scanner_test.go
+++ b/src/pkg/encoding/json/scanner_test.go
@@ -186,11 +186,12 @@ func TestNextValueBig(t *testing.T) {
 	}\n
 }\n

+var benchScan scanner
+\n
 func BenchmarkSkipValue(b *testing.B) {
 	initBig()\n
-\tvar scan scanner
+\tvar scan scanner\n
 	for i := 0; i < b.N; i++ {\n
-\t\tnextValue(jsonBig, &scan)\n
+\t\tnextValue(jsonBig, &benchScan)\n
 	}\n
 	b.SetBytes(int64(len(jsonBig)))\n
 }\n

コアとなるコードの解説

このコミットにおけるコードの変更は非常にシンプルですが、ベンチマークの正確性に大きな影響を与えます。

  1. +var benchScan scanner:

    • この行は、BenchmarkSkipValue関数の定義の直前、つまりパッケージレベルでbenchScanという名前のscanner型の変数を新しく宣言しています。
    • パッケージレベルで宣言された変数は、プログラムの実行開始時に一度だけ初期化され、そのライフサイクルはプログラムの終了まで続きます。
    • これにより、benchScanはベンチマークの実行前に一度だけメモリに割り当てられ、以降のベンチマークループの各イテレーションで再利用されるようになります。
  2. -var scan scanner:

    • この行は、BenchmarkSkipValue関数内のベンチマークループ(for i := 0; i < b.N; i++)の直前にあったscanner変数の宣言を削除しています。
    • 変更前は、この行がループの各イテレーションで実行され、scanという新しいscannerインスタンスが毎回作成されていました。これがガベージコレクションのオーバーヘッドの原因となっていました。
  3. -\t\tnextValue(jsonBig, &scan) から +\t\tnextValue(jsonBig, &benchScan):

    • ベンチマークループ内でnextValue関数を呼び出す際に、以前はループ内で毎回新しく作成されていたscan変数のアドレスを渡していましたが、変更後はパッケージレベルで宣言されたbenchScan変数のアドレスを渡すように修正されています。
    • これにより、nextValue関数は常に同じscannerインスタンスを操作することになり、ループ内でのメモリ割り当てが完全に排除されます。

この変更により、BenchmarkSkipValuescannerの割り当てやGCのオーバーヘッドに影響されることなく、純粋にnextValue関数がJSON値をスキップする処理のパフォーマンスを測定できるようになりました。

関連リンク

参考にした情報源リンク