[インデックス 13305] ファイルの概要
このコミットは、Go言語の標準ライブラリである go/parser
パッケージのベンチマークを test/bench/go1
ディレクトリに追加するものです。これにより、Goパーサーのパフォーマンスを継続的に測定し、回帰を検出するための基盤が提供されます。
コミット
commit 64236820193864cdb6ad28dc475626337cf18a23
Author: Dave Cheney <dave@cheney.net>
Date: Thu Jun 7 10:23:45 2012 +1000
test/bench/go1: add go/parser benchmark
As discussed in
https://groups.google.com/d/msg/golang-dev/Na9XE6mcQyY/zbeBI7R-vnoJ
Here is a static copy of the go/parser benchmark. I ended up using
fancy encodings because the original parser.go had a number of `s
scattered throughout which made it hard to embed the source directly.
Curiously on my laptop this benchmark always scores roughly 10% higher
than the standalone benchmark. This may be down to the generation of
the fasta data set triggering the cpu governor to raise the cpu speed.
However the benchmark is consistent with itself across multiple runs.
R=golang-dev, minux.ma, rsc
CC=golang-dev
https://golang.org/cl/6305055
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/64236820193864cdb6ad28dc475626337cf18a23
元コミット内容
test/bench/go1: add go/parser benchmark
このコミットは、go/parser
のベンチマークを追加します。
golang-dev
メーリングリストでの議論(https://groups.google.com/d/msg/golang-dev/Na9XE6mcQyY/zbeBI7R-vnoJ)で議論されたように、go/parser
ベンチマークの静的コピーです。元の parser.go
には多くのバッククォート(`)が散りばめられており、ソースを直接埋め込むのが難しかったため、凝ったエンコーディングを使用することになりました。
興味深いことに、私のラップトップでは、このベンチマークはスタンドアロンのベンチマークよりも常に約10%高いスコアを出します。これは、fasta データセットの生成がCPUガバナーをトリガーしてCPU速度を上げるためかもしれません。しかし、ベンチマークは複数回実行してもそれ自体で一貫しています。
変更の背景
Go言語のコンパイラやツールチェーンのパフォーマンスは、開発速度とユーザーエクスペリエンスに直結する重要な要素です。特に、ソースコードを解析する go/parser
は、コンパイル、go fmt
、go vet
、IDEの機能など、多くのGoツールの中核をなすコンポーネントです。
このコミットの背景には、go/parser
のパフォーマンスを継続的に監視し、将来の変更がパーサーの速度に悪影響を与えないようにするという明確な意図があります。コミットメッセージで言及されている golang-dev
メーリングリストでの議論は、このベンチマークの必要性と実装方法についてコミュニティ内で検討が行われたことを示唆しています。
具体的な課題として、parser.go
のソースコード自体をベンチマークの入力として使用する際に、コード内に含まれるバッククォート(`)がGoの文字列リテラルとして直接埋め込むことを困難にしていた点が挙げられます。これは、Goの生文字列リテラル(バッククォートで囲まれた文字列)がバッククォート自体を含むことができないという言語仕様によるものです。このため、ベンチマークデータをコードに埋め込むための工夫が必要となりました。
また、ベンチマークの実行環境によるパフォーマンスのばらつき(特にCPUガバナーの影響)についても言及されており、ベンチマークの信頼性と解釈に関する考慮事項が示されています。
前提知識の解説
Go言語の go/parser
パッケージ
go/parser
パッケージは、Go言語のソースコードを解析し、抽象構文木(AST: Abstract Syntax Tree)を構築するためのGo標準ライブラリの一部です。Goコンパイラ、go fmt
(コードフォーマッタ)、go vet
(静的解析ツール)、Go言語をサポートするIDE(統合開発環境)など、多くのGoツールがこのパッケージを利用してGoコードを理解し、処理します。
- AST (Abstract Syntax Tree): ソースコードの構造を木構造で表現したものです。プログラムの各要素(変数宣言、関数呼び出し、制御構造など)がノードとして表現され、それらの関係がエッジで結ばれます。
go/parser
は、Goのソースコードを読み込み、このASTを生成します。 go/token
パッケージ:go/parser
と密接に関連しており、Go言語のトークン(キーワード、識別子、演算子など)やソースコード上の位置情報(ファイル、行番号、列番号)を管理します。parser.ParseFile
関数は、token.FileSet
を引数として受け取り、解析中に遭遇したトークンの位置情報を記録します。
Go言語のベンチマーク
Go言語には、標準ライブラリの testing
パッケージにベンチマーク機能が組み込まれています。これにより、Goプログラムのパフォーマンスを測定し、最適化の効果を評価したり、パフォーマンスの回帰を検出したりすることができます。
- ベンチマーク関数の定義: ベンチマーク関数は
BenchmarkXxx(*testing.B)
という形式で定義されます。 *testing.B
: ベンチマーク実行のためのコンテキストを提供します。b.N
: ベンチマーク関数が実行されるイテレーション回数。ベンチマークシステムが自動的に調整し、統計的に有意な結果が得られるようにします。b.SetBytes(int64)
: 処理されたバイト数を設定します。これにより、go test -bench=.
実行時に「N B/op」(1操作あたりのバイト数)というメトリクスが表示され、スループットの評価に役立ちます。b.ResetTimer()
: タイマーをリセットします。セットアップコードの実行時間を測定から除外するために使用されます。b.StopTimer()
/b.StartTimer()
: タイマーの一時停止と再開。
- 実行方法:
go test -bench=.
コマンドで実行します。特定のベンチマークのみを実行する場合はgo test -bench=BenchmarkParse
のように指定します。 - CPUガバナー: オペレーティングシステムやハードウェアの機能で、CPUの周波数を動的に調整して消費電力や発熱を管理します。ベンチマーク実行中にCPUガバナーが介入すると、ベンチマーク結果にばらつきが生じることがあります。例えば、ベンチマークの開始時にCPUが低周波数で動作し、負荷が上がると高周波数に切り替わることで、初期の実行が遅く、後続の実行が速くなる可能性があります。
データエンコーディング(bzip2
と base64
)
bzip2
: 高い圧縮率を誇るデータ圧縮アルゴリズムです。このコミットでは、大きなソースコードファイルを効率的に格納するために使用されています。base64
: バイナリデータをASCII文字列に変換するエンコーディング方式です。テキストベースの環境(この場合はGoのソースコードファイル)にバイナリデータを埋め込む際に一般的に使用されます。bzip2
で圧縮されたバイナリデータをGoのソースコードに文字列として埋め込むためにbase64
が利用されています。
技術的詳細
このコミットは、go/parser
のベンチマークをGoの標準ベンチマークスイートに追加するために、以下の技術的なアプローチを採用しています。
- ベンチマーク対象の選定:
go/parser
パッケージのParseFile
関数がベンチマークの主要なターゲットです。これは、Goソースコードの解析という中核的な機能のパフォーマンスを測定するためです。 - ベンチマークデータの埋め込み:
- ベンチマークの入力として、実際の
go/parser/parser.go
のソースコードが使用されています。これは、実際のコードベースに近いデータでベンチマークを行うことで、より現実的なパフォーマンス測定を可能にするためです。 - しかし、
parser.go
内に多数のバッククォート(`)が含まれているため、Goの生文字列リテラルとして直接埋め込むことができませんでした。 - この問題を解決するため、以下の多段階エンコーディングが採用されました。
parser.go
の内容をbzip2
で圧縮します。これにより、データのサイズが削減され、バイナリデータとなります。- 圧縮されたバイナリデータを
base64
でエンコードします。これにより、バイナリデータがASCII文字列に変換され、Goの通常の文字列リテラルとしてソースコードに埋め込むことが可能になります。
- このエンコードされたデータは
parserdata_test.go
という新しいファイルにparserbz2_base64
という変数として格納されています。
- ベンチマークの入力として、実際の
- ベンチマークの実行ロジック:
parser_test.go
内のmakeParserBytes
関数が、エンコードされたデータ(parserbz2_base64
)をデコードし、元のparser.go
のバイト列を復元します。この処理はベンチマークのセットアップフェーズ(init
関数内で呼び出される)で行われるため、ベンチマークの測定時間には含まれません。BenchmarkParse
関数が実際のベンチマークを実行します。b.SetBytes(int64(len(parserbytes)))
を呼び出すことで、go/parser
が処理するバイト数をベンチマーク結果に表示させ、スループットの評価を可能にしています。- ループ内で
parser.ParseFile(token.NewFileSet(), "", parserbytes, parser.ParseComments)
を繰り返し呼び出し、parser.go
の解析にかかる時間を測定します。 - エラーハンドリングも含まれており、解析エラーが発生した場合はベンチマークを失敗させます。
- パフォーマンスの観察と考察:
- コミットメッセージでは、ベンチマークがスタンドアロンで実行した場合よりも、このベンチマークスイート内で実行した場合の方が約10%高いスコアを出すという興味深い観察が述べられています。
- これに対する仮説として、ベンチマークデータの生成(
makeParserBytes
関数によるデコード)がCPUガバナーをトリガーし、CPUの周波数を引き上げた可能性が挙げられています。これは、ベンチマークのウォームアップ効果や、システム全体の負荷状況がベンチマーク結果に影響を与える可能性があることを示しています。 - しかし、ベンチマーク自体は複数回実行しても一貫した結果を示すとされており、相対的なパフォーマンス比較には十分信頼できることが示唆されています。
このアプローチにより、go/parser
のパフォーマンスをGoの公式ベンチマークスイートに統合し、継続的な監視と改善を可能にしています。
コアとなるコードの変更箇所
このコミットでは、以下の2つの新しいファイルが追加されています。
-
test/bench/go1/parser_test.go
:makeParserBytes()
関数:parserdata_test.go
に埋め込まれたbase64
エンコードされたbzip2
圧縮データをデコードし、元のparser.go
のバイト列を復元します。BenchmarkParse(b *testing.B)
関数:go/parser.ParseFile
を使用して復元されたバイト列を解析するベンチマークを実行します。b.SetBytes
を呼び出して処理バイト数を報告します。
-
test/bench/go1/parserdata_test.go
:parserbz2_base64
変数:src/pkg/go/parser/parser.go
の内容をbzip2 -9
で圧縮し、さらにbase64
でエンコードした文字列データが格納されています。このデータは、ベンチマークの入力として使用されます。
コアとなるコードの解説
test/bench/go1/parser_test.go
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package go1
// go parser benchmark based on go/parser/performance_test.go
import (
"compress/bzip2" // bzip2圧縮データを読み込むためのパッケージ
"encoding/base64" // base64エンコードされたデータをデコードするためのパッケージ
"go/parser" // Goソースコードを解析するためのパッケージ
"go/token" // Goトークンとソース位置を扱うためのパッケージ
"io" // I/Oプリミティブ
"io/ioutil" // ユーティリティ関数(ReadAllなど)
"strings" // 文字列操作
"testing" // Goのテストとベンチマークフレームワーク
)
var (
parserbytes = makeParserBytes() // ベンチマーク入力となるバイト列。パッケージ初期化時に生成される。
)
// makeParserBytes は、埋め込まれたbase64+bzip2エンコードされたデータをデコードし、
// go/parser/parser.go の元のバイト列を返す。
func makeParserBytes() []byte {
var r io.Reader
// base64エンコードされた文字列をstrings.NewReaderで読み込む
r = strings.NewReader(parserbz2_base64)
// base64デコーダでラップ
r = base64.NewDecoder(base64.StdEncoding, r)
// bzip2デコーダでラップ
r = bzip2.NewReader(r)
// 全てのデータを読み込む
b, err := ioutil.ReadAll(r)
if err != nil {
// エラーが発生した場合はパニック(ベンチマークのセットアップフェーズなので)
panic(err)
}
return b
}
// BenchmarkParse は go/parser.ParseFile のパフォーマンスをベンチマークする。
func BenchmarkParse(b *testing.B) {
// 処理されるバイト数を設定。これにより、ベンチマーク結果に「N B/op」が表示される。
b.SetBytes(int64(len(parserbytes)))
// b.N 回ループしてベンチマークを実行
for i := 0; i < b.N; i++ {
// go/parser.ParseFile を呼び出して、parserbytes を解析する。
// token.NewFileSet() で新しいファイルセットを作成。
// "" はファイル名(ベンチマークでは不要)。
// parser.ParseComments はコメントも解析対象に含めるフラグ。
if _, err := parser.ParseFile(token.NewFileSet(), "", parserbytes, parser.ParseComments); err != nil {
// 解析エラーが発生した場合はベンチマークを失敗させる。
b.Fatalf("benchmark failed due to parse error: %s", err)
}
}
}
test/bench/go1/parserdata_test.go
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Input for parser benchmark.
// This was generated by starting with a the contents of
// src/pkg/go/parser/parser.go at rev 9b455eb64690, then
// compressing with bzip2 -9, then encoding to base64.
// We compile the data into the binary so that the benchmark is
// a stand-alone binary that can be copied easily from machine to
// machine. parser_test.go decodes this during init.
package go1
// parserbz2_base64 は、go/parser/parser.go の内容をbzip2で圧縮し、
// base64でエンコードした文字列データ。
// この変数は非常に長く、実際のファイルには大量のbase64文字列が続く。
var parserbz2_base64 = "QlpoOTFBWSZTWd3QmOEAIYdfgHwwf//6P7/v/+/////+YEhcAAAB7hwvVWWaZT1X0dt999296z5B" +
"3mcQqlBVVVB7hnASWJoFGq9jlHvZHRbdfIB0Hz6fW+jrz4dueD73be6c33tG7la1O9d154ntzzk7" +
// ... (以下、非常に長いbase64エンコードされた文字列が続く)
"3QmOE"
関連リンク
- Go言語の
go/parser
パッケージのドキュメント: https://pkg.go.dev/go/parser - Go言語の
testing
パッケージのドキュメント: https://pkg.go.dev/testing - Go言語の
go/token
パッケージのドキュメント: https://pkg.go.dev/go/token - Go言語のベンチマークに関する公式ブログ記事(古いですが概念は同じ): https://go.dev/blog/benchmarking
参考にした情報源リンク
- コミットメッセージに記載されている
golang-dev
メーリングリストの議論: https://groups.google.com/d/msg/golang-dev/Na9XE6mcQyY/zbeBI7R-vnoJ - Go言語の公式ドキュメントおよびパッケージドキュメント
- Go言語のベンチマークに関する一般的な知識
bzip2
およびbase64
エンコーディングに関する一般的な知識- GitHub上のGoリポジトリのコミット履歴