[インデックス 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
インスタンスが再利用されるようになります。
この変更によって得られる主な利点は以下の通りです。
- GCオーバーヘッドの削減:
scanner
の割り当てがループ外に移動したことで、ベンチマークループ内でメモリ割り当てが発生しなくなります。これにより、GCがトリガーされる頻度が大幅に減少し、GCによる一時停止がベンチマーク結果に与える影響が最小限に抑えられます。 - ベンチマークの一貫性向上: GCの介入が減ることで、ベンチマークの実行ごとに測定される時間がより安定し、一貫性のある結果が得られるようになります。これにより、
SkipValue
処理自体の純粋なパフォーマンスをより正確に評価できるようになります。 - リソース再利用の促進: このパターンは、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
コアとなるコードの解説
このコミットにおけるコードの変更は非常にシンプルですが、ベンチマークの正確性に大きな影響を与えます。
-
+var benchScan scanner
:- この行は、
BenchmarkSkipValue
関数の定義の直前、つまりパッケージレベルでbenchScan
という名前のscanner
型の変数を新しく宣言しています。 - パッケージレベルで宣言された変数は、プログラムの実行開始時に一度だけ初期化され、そのライフサイクルはプログラムの終了まで続きます。
- これにより、
benchScan
はベンチマークの実行前に一度だけメモリに割り当てられ、以降のベンチマークループの各イテレーションで再利用されるようになります。
- この行は、
-
-var scan scanner
:- この行は、
BenchmarkSkipValue
関数内のベンチマークループ(for i := 0; i < b.N; i++
)の直前にあったscanner
変数の宣言を削除しています。 - 変更前は、この行がループの各イテレーションで実行され、
scan
という新しいscanner
インスタンスが毎回作成されていました。これがガベージコレクションのオーバーヘッドの原因となっていました。
- この行は、
-
-\t\tnextValue(jsonBig, &scan)
から+\t\tnextValue(jsonBig, &benchScan)
:- ベンチマークループ内で
nextValue
関数を呼び出す際に、以前はループ内で毎回新しく作成されていたscan
変数のアドレスを渡していましたが、変更後はパッケージレベルで宣言されたbenchScan
変数のアドレスを渡すように修正されています。 - これにより、
nextValue
関数は常に同じscanner
インスタンスを操作することになり、ループ内でのメモリ割り当てが完全に排除されます。
- ベンチマークループ内で
この変更により、BenchmarkSkipValue
はscanner
の割り当てやGCのオーバーヘッドに影響されることなく、純粋にnextValue
関数がJSON値をスキップする処理のパフォーマンスを測定できるようになりました。