テスト & ベンチマーク実施レポート
対象リポジトリ: example.com/lalrone_multi_statement_parser(lalrone_multi_statement_parser4)
このレポートは、expression/qualified_name/table_referenceの3パッケージにテスト・ベンチマークを追加した後にリポジトリ全体の go test と go test -bench を実行した結果をまとめたものである。各パッケージ個別の設計・アルゴリズム解説は既存の */docs/*_ALGORITHM_AND_BENCHMARKS.md を参照し、本レポートは「今回の変更を含めた全パッケージの現在値」に焦点を当てる。
実行環境
| 項目 | 値 |
|---|---|
| OS / Arch | linux / amd64 |
| CPU | AMD Ryzen 5 5500U with Radeon Graphics(6コア12スレッド) |
| Go | go1.27.0 |
| テストコマンド | go test ./... -v |
| ベンチマークコマンド | go test <pkg> -run '^$' -bench . -benchmem -count 5(パッケージごとに実行) |
| 反復回数 | 各ベンチマーク5回(-count 5)、下表は5回の単純平均 |
1. テスト結果
go test ./... -v
| パッケージ | 結果 | PASSしたテスト数 |
|---|---|---|
lalrone_multi_statement_parser(ルート) |
ok | 16 |
lalrone_table_generator |
ok | 16 |
lalrone_table_generator/symbol_set |
ok | 4 |
semantic_value |
ok | 4 |
sql_lexer |
ok | 20 |
state_stack |
ok | 5 |
statement |
ok | 6 |
value_stack |
ok | 4 |
expression(今回追加) |
ok | 5 |
qualified_name(今回追加) |
ok | 3 |
table_reference(今回追加) |
ok | 3 |
| 合計 | 全てPASS | 86 / 86 |
FAIL は0件。go vet ./... と gofmt -l . も警告なし。
2. ベンチマーク結果(5回平均)
以下は各ベンチマークの5サンプル(-count 5)の単純平均。生データは各パッケージで go test <pkg> -run '^$' -bench . -benchmem -count 5 を実行すれば再現できる。
2.1 value_stack / state_stack(スタック基本操作)
| ベンチマーク | ns/op | B/op | allocs/op |
|---|---|---|---|
ValueStackPush(容量内) |
35.0 | 0 | 0 |
ValueStackPushWithGrow(毎回grow) |
7025 | 18560 | 6 |
ValueStackPushPop |
247 | 256 | 1 |
ValueStackPeek |
1.01 | 0 | 0 |
StateStackPush(容量内) |
57.4 | 0 | 0 |
StateStackPushWithGrow(毎回grow) |
6878 | 16128 | 6 |
StateStackPushPop |
254 | 256 | 1 |
StateStackPeek |
1.02 | 0 | 0 |
StateStackPopCount |
57.6 | 0 | 0 |
容量内Pushは0alloc(事前確保したelements配列への書き込みのみ)。growを毎回強制するケースは2桁〜3桁ns重くなり、確保サイズ相応のB/opが乗る。Peekはいずれも約1ns/0allocで、インライン化されたフィールド参照とほぼ同等のコスト。
2.2 semantic_value
| ベンチマーク | ns/op | B/op | allocs/op |
|---|---|---|---|
Token |
30.0 | 80 | 1 |
Expression |
30.4 | 80 | 1 |
Statement |
30.2 | 80 | 1 |
Getters(4アクセサ一括) |
1.90 | 0 | 0 |
注目点: 各コンストラクタは80B/1allocで、既存ドキュメント(semantic_value/docs/...)に記載の48B/1allocから増加している。これは今回SELECT_LIST種別(columns []string + selectAll bool)をSemanticValueに追加したことで共用体全体のサイズが増えたため(タグ付き共用体は使わないフィールドがあっても構造体全体を確保する設計のため、既存コンストラクタのコストにも波及する)。
2.3 statement
| ベンチマーク | ns/op | B/op | allocs/op |
|---|---|---|---|
Select |
67.0 | 112 | 2 |
Insert |
47.6 | 96 | 1 |
Update |
48.3 | 96 | 1 |
Delete |
48.1 | 96 | 1 |
Drop |
50.6 | 96 | 1 |
GetKind |
1.50 | 0 | 0 |
Getters(4アクセサ一括) |
2.02 | 0 | 0 |
注目点: Selectだけ2allocs(他は1alloc)。Statement構造体自体の確保に加え、ベンチ内で渡している[]string{"id"}という列名スライスリテラル自体がヒープへエスケープしてもう1回確保されるため。実際の構文解析パス(reduce()のcase 9/10)でもColumnListを1列ずつappendで積み上げていくため、複数カラムのSELECTは列数に応じてスライスの再確保が発生し得る(詳細はコードレビューの回答参照)。
2.4 sql_lexer
| ベンチマーク | ns/op | B/op | allocs/op | MB/s |
|---|---|---|---|---|
Tokenize_Short(短いSELECT文) |
652 | 456 | 15 | 56.7 |
Tokenize_Long(長い複合WHERE) |
141,873 | 80,913 | 4,794 | 47.2 |
Tokenize_Numbers |
8,282 | 9,240 | 207 | 193.1 |
Tokenize_Strings |
157,776 | 62,041 | 6,607 | 26.6 |
Tokenize_Identifiers |
25,715 | 12,440 | 407 | 132.2 |
文字列リテラルのトークナイズ(Tokenize_Strings)が最もスループットが低い(26.6 MB/s)。これはreadString()がbuilder += string(c)という1文字ずつの文字列連結でトークンテキストを構築しており(sql_lexer.go)、Goの文字列は不変なので毎回新しいバッファへコピーが発生するため。数値・識別子はスライスs.input[start:s.position]をそのまま参照するだけなので大幅に速い。
2.5 lalrone_table_generator/symbol_set
| ベンチマーク | ns/op | B/op | allocs/op |
|---|---|---|---|
Add(n=10) |
6.0 | 0 | 0 |
Add(n=100) |
45.4 | 0 | 0 |
Add(n=1000) |
412.9 | 0 | 0 |
Contains(n=10) |
34.3 | 0 | 0 |
Contains(n=100) |
353.4 | 0 | 0 |
Contains(n=1000) |
3483.0 | 0 | 0 |
Add/Containsともにnに対してほぼ線形(n=10→100→1000で約7〜10倍ずつ増加)で、内部実装が線形走査(スライスベースの重複チェック)であることと整合する。0allocなのは追加先のSymbolSetを使い回すベンチ設計のため。
2.6 lalrone_table_generator(LALR(1)表構築パイプライン)
| ベンチマーク | ns/op | B/op | allocs/op |
|---|---|---|---|
NewLALR1TableGenerator |
1,165 | 1,440 | 43 |
BuildFirstSets |
16,471 | 728 | 39 |
BuildCanonicalStates |
15,044,183 | 4,160,649 | 192,323 |
MergeToLalrStates |
2,931,457 | 846,528 | 11,112 |
BuildLalrTables |
392,129 | 37,080 | 1,499 |
FullPipeline(New→...→BuildLalrTables 一式) |
18,617,570 | 5,046,424 | 205,016 |
Closure(単発呼び出し) |
3,427 | 1,544 | 77 |
注目点: BuildCanonicalStates(約15.0ms)がFullPipeline(約18.6ms)の8割強を占める、パイプライン中の圧倒的なボトルネック。カノニカルLR(1)状態集合の構築は状態数×クロージャ計算のコストで、今回SELECT_LIST/ColumnList用に4生成規則を追加した結果カノニカル状態数が140→145、LALR併合後の状態数が72→77に増加しており(前回のレビュー参照)、文法追加のたびにこの部分の相対コストが上がっていく。NewLALR1MultiStatementParserはテーブルをコンストラクタで1回だけ構築して使い回す設計になっており、この約18.6msのコストはSQL文1件ごとには発生しない。
2.7 lalrone_table_generator(表を使ったパース: runParseヘルパー経由)
| ベンチマーク | ns/op | B/op | allocs/op |
|---|---|---|---|
ParseSelectStatement |
1,883 | 112 | 3 |
ParseInsertStatement |
649 | 112 | 3 |
ParseUpdateStatement |
2,349 | 240 | 4 |
ParseDeleteStatement |
1,767 | 112 | 3 |
ParseDropStatement |
461 | 48 | 2 |
ParseSelectStatementWithComplexWhere |
9,436 | 240 | 4 |
こちらはASTを組み立てない、表の妥当性検証用の軽量シミュレータ(runParse)による計測。
2.8 ルートパッケージ(Parse() = 実際にASTを組み立てる本番パス)
| ベンチマーク | ns/op | B/op | allocs/op |
|---|---|---|---|
ParseSelect |
2,726 | 1,344 | 18 |
ParseInsert |
609 | 576 | 7 |
ParseUpdate |
2,913 | 1,408 | 18 |
ParseDelete |
2,450 | 1,168 | 15 |
ParseDrop |
423 | 416 | 5 |
ParseSelectWithComplexWhere(括弧+論理+比較) |
10,882 | 4,192 | 56 |
TokenizeAndParseSelect(Tokenize込み) |
3,562 | 1,776 | 32 |
runParse(2.7節)と比べ、Parse()(本番パス)は同じ文でも allocs/op が大きい(例: ParseSelectStatement 3allocs → ParseSelect 18allocs)。これはrunParseが状態遷移の正否しか見ないのに対し、Parse()はreduce()内でsemantic_value.*・expression.*・statement.*の各値オブジェクトを実際に構築しながら進むため。
2.9 qualified_name
| ベンチマーク | ns/op | B/op | allocs/op |
|---|---|---|---|
NewQualifiedNameWithQualifier |
20.9 | 32 | 1 |
NewQualifiedNameWithoutQualifier |
20.2 | 32 | 1 |
StringWithQualifier("t.id"結合) |
28.4 | 4 | 1 |
StringWithoutQualifier(結合なし) |
1.52 | 0 | 0 |
Getters(3アクセサ一括) |
1.51 | 0 | 0 |
詳細と考察はqualified_nameのドキュメントを参照。
2.10 table_reference
| ベンチマーク | ns/op | B/op | allocs/op |
|---|---|---|---|
NewTableReferenceWithAlias |
20.9 | 32 | 1 |
NewTableReferenceWithoutAlias |
20.3 | 32 | 1 |
StringWithAlias("users AS u"結合) |
36.7 | 16 | 1 |
StringWithoutAlias(結合なし) |
1.01 | 0 | 0 |
Getters(3アクセサ一括) |
1.52 | 0 | 0 |
qualified_nameと同型のフラット2-stringラッパーだが、区切り文字列が" AS "(4バイト)で"."(1バイト)より長いぶんStringWithAliasのB/opが大きい。詳細はtable_referenceのドキュメントを参照。
2.11 expression(唯一の再帰的データ構造)
| ベンチマーク | ns/op | B/op | allocs/op |
|---|---|---|---|
Identifier / Number / Binary(各コンストラクタ) |
25.1〜25.6 | 64 | 1 |
Getters(5アクセサ一括) |
2.02 | 0 | 0 |
StringDepth1(BINARYノード1個) |
55.6 | 16 | 1 |
StringDepth5(BINARYノード9個) |
564 | 304 | 9 |
StringDepth10(BINARYノード19個) |
1392 | 1072 | 19 |
注目点: Expressionはleft/rightで自分自身を指す木構造で、このリポジトリの他パッケージ(semantic_value/statementのようなフラットなタグ付き共用体)とは質的に異なる。String()の再帰呼び出しはBINARYノード1個につき1回の文字列結合(1alloc)を行うため、allocs/opは木のBINARYノード数と正確に一致する。1ノードあたりのコストは深さとともに緩やかに増加する(55.6→62.7→73.3 ns/node)——外側のノードほど、既に組み上がった長い部分文字列をコピーするコストを払うため。詳細はexpressionのドキュメントを参照。
3. 補足: SelectList実装メモ
今回のベンチマーク対象コードは、直前のセッションで追加した以下の変更を含む:
- 文法:
SelectStatement -> SELECT SelectList FROM IDENTIFIER WhereClause、SelectList -> * | ColumnList、ColumnList -> IDENTIFIER | ColumnList COMMA IDENTIFIER(lalrone_table_generator.go、production #6〜#10) semantic_value.SemanticValueにSELECT_LIST種別(columns []string+selectAll bool)を追加statement.Selectのシグネチャを(column, table string, where)から(columns []string, selectAll bool, table string, where)に変更- 併せて、文法に未使用のまま残っていた
STATEMENT_LIST/VALUE_LIST/VALUE/ASSIGNMENT_LIST/ASSIGNMENT/SEMICOLONシンボル宣言(複数文・複数カラムINSERT/UPDATE用の未実装機能の残骸)を削除
これらの変更後もカノニカルLR(1)状態数145・LALR(1)状態数77で衝突(shift/reduce・reduce/reduce)は発生していない(TestBuildLalrTablesNoConflictsでPASS)。
4. 再現手順
# 全パッケージのテスト go test ./... -v # パッケージごとのベンチマーク(-count 5 で本レポートと同条件) go test . -run '^$' -bench . -benchmem -count 5 go test ./lalrone_table_generator/... -run '^$' -bench . -benchmem -count 5 go test ./semantic_value/... -run '^$' -bench . -benchmem -count 5 go test ./statement/... -run '^$' -bench . -benchmem -count 5 go test ./sql_lexer/... -run '^$' -bench . -benchmem -count 5 go test ./value_stack/... -run '^$' -bench . -benchmem -count 5 go test ./state_stack/... -run '^$' -bench . -benchmem -count 5 go test ./lalrone_table_generator/symbol_set/... -run '^$' -bench . -benchmem -count 5 go test ./expression/... -run '^$' -bench . -benchmem -count 5 go test ./qualified_name/... -run '^$' -bench . -benchmem -count 5 go test ./table_reference/... -run '^$' -bench . -benchmem -count 5