[インデックス 18804] ファイルの概要
このコミットは、Go言語の正規表現パッケージ(regexp
)に、RE2ライブラリから着想を得た「ワンパス最適化」を導入するものです。この最適化により、特定の正規表現パターンにおいて約2.3倍の高速化が実現されます。
コミット
commit 76236ef13684fd63555ae4be90ca31e94eda670f
Author: David Covert <davidhcovert@gmail.com>
Date: Fri Mar 7 15:30:02 2014 -0500
regexp: add one-pass optimization from RE2
This produces about a 2.3x speedup for patterns
that can be handled this way.
LGTM=rsc
R=rsc
CC=golang-codereviews
https://golang.org/cl/13345046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/76236ef13684fd63555ae4be90ca31e94eda670f
元コミット内容
このコミットは、Go言語の正規表現エンジンにRE2由来のワンパス最適化を実装し、特定のパターンマッチングを高速化することを目的としています。具体的には、正規表現の内部表現であるプログラム(syntax.Prog
)を解析し、ワンパスで処理可能なパターンであれば、専用の高速なマッチングパスを使用します。これにより、該当するパターンでは約2.3倍の速度向上が見込まれます。
変更の背景
Go言語のregexp
パッケージは、GoogleのRE2正規表現エンジンに基づいています。RE2は、バックトラッキングを伴わない決定性有限オートマトン(DFA)ベースのマッチングアルゴリズムを採用しており、最悪計算量が入力文字列の長さに線形であるという特徴があります。しかし、一部の正規表現パターン、特に先読みや後読み、または複雑なグループ化を伴わない単純なパターンでは、DFAベースのアプローチでもオーバーヘッドが生じることがあります。
このコミットの背景には、このような単純なパターンに対して、さらに効率的なマッチング手法を導入し、パフォーマンスを向上させたいという意図があります。RE2には、特定の正規表現が「ワンパス」で処理可能かどうかを判定し、その場合はよりシンプルな状態遷移でマッチングを行う最適化が存在します。この最適化をGoのregexp
パッケージにも取り入れることで、より広範な正規表現のユースケースで高速化を実現しようとしています。
前提知識の解説
正規表現エンジンとオートマトン
正規表現のマッチングは、通常、有限オートマトン(Finite Automaton, FA)の概念に基づいて行われます。
-
非決定性有限オートマトン (NFA: Nondeterministic Finite Automaton):
- 同じ入力記号に対して複数の状態に遷移する可能性があるオートマトンです。
- バックトラッキングを伴う実装が多く、最悪計算量が正規表現の長さに指数関数的に比例する場合があります(例: Perl, Pythonの
re
モジュール)。 - しかし、NFAはキャプチャグループや後方参照といった高度な正規表現機能の実装が容易です。
-
決定性有限オートマトン (DFA: Deterministic Finite Automaton):
- 同じ入力記号に対して常に一意の状態に遷移するオートマトンです。
- バックトラッキングが不要で、入力文字列の長さに線形時間でマッチングが完了します。最悪計算量が保証されるため、予測可能なパフォーマンスを提供します。
- RE2やGoの
regexp
パッケージはDFAベースです。DFAはNFAよりも構築が複雑で、キャプチャグループの扱いが難しいという側面もあります。
RE2とGoのregexp
パッケージ
Goのregexp
パッケージは、RE2ライブラリの設計思想とアルゴリズムをベースにしています。RE2は、DFAベースのアプローチにより、高速かつ安全な正規表現マッチングを提供します。特に、正規表現の複雑さに依存しない線形時間のマッチングは、サービス拒否攻撃(ReDoS)のリスクを低減する上で重要です。
ワンパス最適化 (One-Pass Optimization)
ワンパス最適化とは、正規表現のマッチングにおいて、入力文字列を一度だけ走査する(ワンパス)ことで結果を得る手法です。これは、正規表現が持つ特定の構造(例えば、選択肢が入力文字によって明確に区別できる場合など)を利用して、バックトラッキングや複雑な状態管理を不要にするものです。
通常のDFAベースのマッチングでも線形時間ですが、ワンパス最適化はさらにオーバーヘッドを削減し、より直接的な状態遷移でマッチングを進めることができます。これは、特にシンプルなリテラル文字列のプレフィックスを持つパターンや、選択肢が明確に分離されているパターンで効果を発揮します。
例えば、^abc(d|e)*$
のような正規表現では、^abc
というプレフィックスが確定しており、その後の(d|e)*
もd
かe
のどちらか一文字を消費するだけで次の状態が決定できます。このようなパターンはワンパスで処理可能です。
技術的詳細
このコミットでは、regexp/syntax
パッケージに正規表現の内部表現であるProg
(プログラム)を解析し、ワンパス実行が可能かどうかを判定するロジックが追加されています。
-
syntax.Prog
の拡張:syntax.Inst
構造体にNext []uint32
フィールドが追加されました。これは、ワンパス実行時に次の命令ポインタ(PC)を格納するために使用されます。syntax.InstOp
にInstLast
が追加され、命令の種類が増えたことを示しています。syntax.Prog
にCompileOnePass()
メソッドが追加されました。これがワンパス最適化の主要な入り口となります。
-
CompileOnePass()
メソッド:- このメソッドは、現在の
Prog
がワンパス実行可能かどうかを判定し、可能であればワンパス実行用の新しいProg
を返します。不可能であればsyntax.NotOnePass
(nil
)を返します。 - ワンパス実行の条件として、正規表現がアンカー(
^
)で開始されていること、およびInstMatch
に到達する全てのパスがEmptyEndText
(行末)で終わる必要があることなどがチェックされます。 - 内部では、
onePassCopy()
で元のProg
のコピーを作成し、makeOnePass()
でワンパス実行可能な形に変換を試みます。 makeOnePass()
は再帰的にプログラムを走査し、InstAlt
(選択肢)命令において、次の入力文字によってどの分岐に進むべきかが一意に決定できるかを検証します。mergeRuneSets
関数は、InstAlt
命令の各分岐がマッチするルーンセットを結合し、重複がないか(曖昧さがないか)をチェックします。重複がある場合はワンパス実行不可能と判断されます。InstRune
命令の処理が強化され、ケースフォールド(大文字小文字を区別しないマッチング)も考慮したルーンセットの生成が行われます。
- このメソッドは、現在の
-
regexp.Regexp
とregexp.machine
の変更:regexp.Regexp
構造体にonepass *syntax.Prog
フィールドが追加され、ワンパス実行可能なプログラムが格納されます。regexp.machine
構造体にもop *syntax.Prog
フィールドが追加され、実行時にワンパスプログラムが利用されます。Regexp.doExecute
メソッド内で、m.op != syntax.NotOnePass
の場合にm.onepass(i, pos)
が呼び出され、ワンパス実行が試みられます。これにより、ワンパス実行可能なパターンでは、従来のDFAベースのマッチングよりも高速なonepass
関数が使用されます。
-
onepass
関数:regexp/exec.go
に追加されたonepass
関数は、ワンパス実行可能な正規表現プログラムを効率的に実行します。- この関数は、入力文字列を先頭から順に走査し、各命令(
InstRune
,InstRune1
,InstRuneAny
,InstRuneAnyNotNL
,InstEmptyWidth
,InstCapture
など)を直接処理します。 InstAlt
やInstAltMatch
命令では、OnePassNext
メソッドを使用して、現在の入力ルーンに基づいて次に進むべき命令ポインタを決定します。これにより、バックトラッキングなしで一意のパスを辿ることができます。- シンプルなリテラルプレフィックスを持つパターンでは、
i.hasPrefix(m.re)
による高速なプレフィックスチェックが行われ、マッチングの開始位置を効率的にスキップできます。
コアとなるコードの変更箇所
-
src/pkg/regexp/exec.go
:machine
構造体にop *syntax.Prog
フィールドを追加。progMachine
関数がonepass
プログラムを受け取るように変更。onepass
関数が新規追加され、ワンパス実行ロジックを実装。doExecute
関数内で、onepass
プログラムが存在する場合にonepass
関数を呼び出すように変更。
-
src/pkg/regexp/regexp.go
:Regexp
構造体にonepass *syntax.Prog
とprefixEnd uint32
フィールドを追加。compile
関数内でprog.CompileOnePass()
を呼び出し、onepass
フィールドに結果を格納。get
関数内でprogMachine
にonepass
プログラムを渡すように変更。
-
src/pkg/regexp/syntax/prog.go
:Inst
構造体にNext []uint32
フィールドを追加。InstOp
にInstLast
を追加し、InstOp
のString()
メソッドを更新。skipNop
関数がpc
も返すように変更。OnePassPrefix()
関数を新規追加。OnePassNext()
関数を新規追加。MatchRunePos()
関数を新規追加し、MatchRune
から分離。queue
構造体と関連メソッド(newQueue
,empty
,next
,clear
,reset
,contains
,insert
,insertNew
)を新規追加。mergeRuneSets()
関数を新規追加。cleanupOnePass()
関数を新規追加。onePassCopy()
関数を新規追加。runeSlice
型と関連メソッド(Len
,Less
,Swap
,Sort
)を新規追加。makeOnePass()
関数を新規追加。walk()
関数を新規追加。find()
関数を新規追加。CompileOnePass()
関数を新規追加。
-
src/pkg/regexp/all_test.go
:- ワンパス最適化のベンチマークテスト(
BenchmarkOnePassShortA
,BenchmarkNotOnePassShortA
など)を新規追加。
- ワンパス最適化のベンチマークテスト(
-
src/pkg/regexp/syntax/prog_test.go
:mergeRuneSets
関数のテスト(TestMergeRuneSet
)を新規追加。CompileOnePass
関数のテスト(TestCompileOnePass
)を新規追加。
コアとなるコードの解説
regexp/syntax/prog.go
におけるワンパスプログラムの構築
このコミットの核心は、正規表現の抽象構文木(AST)から生成されるsyntax.Prog
を、ワンパス実行可能な形式に変換するロジックです。
Prog.CompileOnePass()
関数がその入り口です。この関数は、まず正規表現がアンカー(^
)で始まるなど、ワンパス実行の基本的な条件を満たしているかを確認します。次に、onePassCopy()
でプログラムのコピーを作成し、makeOnePass()
を呼び出して実際の変換処理を行います。
makeOnePass()
は、プログラム内の各命令を再帰的に走査し、特にInstAlt
(選択肢)命令に注目します。ワンパス実行の最大の課題は、a|b
のような選択肢がある場合に、入力文字を見てどちらの分岐に進むべきかを即座に決定できるかどうかです。
makeOnePass()
は、check
関数とmergeRuneSets
関数を組み合わせてこの問題を解決します。
check
関数は、各命令から到達可能なパスを探索し、InstMatch
(マッチ成功)に到達するパスがあるかどうか、およびInstAlt
命令で曖昧さがないかを確認します。mergeRuneSets
関数は、InstAlt
の各分岐がマッチするルーン(文字)の範囲を結合します。もし、異なる分岐が同じルーン範囲にマッチする場合(例:a|a
)、それは曖昧であり、ワンパス実行は不可能と判断されます。この関数は、結合されたルーンセットと、それぞれのルーンセットがどの分岐に対応するかを示すNext
配列を返します。
成功した場合、InstAlt
命令は、入力ルーンに基づいて次に進むべき命令ポインタを直接指し示すNext
配列を持つようになります。これにより、実行時に複雑な状態管理やバックトラッキングなしに、一意のパスを辿ることが可能になります。
regexp/exec.go
におけるワンパス実行
regexp.machine
構造体は、正規表現のマッチングを実行するエンジンです。このコミットでは、machine
にop *syntax.Prog
フィールドが追加され、ワンパス実行可能なプログラムが格納されます。
Regexp.doExecute
メソッドは、マッチングの開始時に、machine
にワンパスプログラムが設定されているかどうかを確認します。もし設定されていれば、新しく追加されたonepass
関数が呼び出されます。
onepass
関数は、非常にシンプルなループで入力文字列を走査します。各ステップで現在の命令を評価し、入力ルーンに基づいて次の命令に直接遷移します。
InstRune
やInstRune1
のような文字マッチング命令では、現在の入力ルーンが命令のルーンセットにマッチするかをチェックし、マッチすれば次の命令に進みます。InstAlt
やInstAltMatch
命令では、inst.OnePassNext(r)
を呼び出し、現在の入力ルーンr
に基づいて、Next
配列から次に進むべき命令ポインタを直接取得します。これにより、複数の分岐の中から正しいパスを迷うことなく選択できます。InstCapture
命令では、キャプチャグループの開始/終了位置を記録します。
この直接的な実行パスにより、従来のDFAベースのマッチングで必要だったキュー管理やスレッド(状態)の管理が不要になり、大幅な高速化が実現されます。
関連リンク
- RE2 GitHubリポジトリ: https://github.com/google/re2
- Go言語
regexp
パッケージドキュメント: https://pkg.go.dev/regexp - Go言語
regexp/syntax
パッケージドキュメント: https://pkg.go.dev/regexp/syntax
参考にした情報源リンク
- RE2: a principled approach to regular expression matching: https://swtch.com/~rsc/regexp/regexp1.html (Rob PikeによるRE2の解説記事)
- Regular Expression Matching Can Be Simple And Fast (but is slow in Java, Perl, PHP, Python, Ruby, ...): https://swtch.com/~rsc/regexp/regexp.html (正規表現エンジンの種類とパフォーマンスに関する解説記事)
- Go CL 13345046: regexp: add one-pass optimization from RE2: https://go-review.googlesource.com/c/go/+/13345046 (このコミットのGoレビューシステム上のページ)