
2026/03/20 3:40
正規表現で全てのマッチを探すことは、常に O(n²) でした。
RSS: https://news.ycombinator.com/rss
要約▶
日本語訳:
主旨
一般的な正規表現エンジンは、単一のマッチに対してのみ線形時間を保証し、そのイテレータはパターンと文字列が信頼できない場合、最悪ケースで (O(m \cdot n^{2})) あるいは指数関数的な爆発(ReDoS 攻撃)を起こす可能性があります。
重要性
二次的な爆発は、バックトラッキングが各位置で多くの枝を試すために発生します(例:長い「b」文字列上の
.*a|b)。バックトラッキングエンジンは指数関数的になることもあります。Thompson の NFA 構築法ではそれを回避できますが、ほとんどのライブラリはデフォルトでバックトラッキングを採用しています。
記事の提案
RE# は二段階アルゴリズムを実装しています:逆方向 DFA が (O(n)) 時間で全ての潜在的開始位置を見つけ、前方 DFA が左端長いマッチを選択し、POSIX の意味論を保持します。強化モードでは、前方パスを活性状態に対する (O(n \cdot S)) スキャンで置き換え、
.*a|b のような悪質なパターンでも長い「b」文字列上で線形時間を保証します。
主な利点
- 任意の正規表現に対して全マッチが線形時間で取得できる。
- 強化モードは安全性を保証し、典型的なワークロードではわずかな遅延(約 3–20 倍)しか生じません。
- 大規模な交互辞書に対して Aho–Corasick を上回る性能を発揮します。RE# は小さくキャッシュフレンドリーな派生 DFA を遅延生成するためです。
現在の制限点
キャプチャグループ、遅延量化子 (
.*?)、ストリーミング API には対応しておらず、マッチ境界のみが返されます。
今後の展開と影響
計画中の拡張としては、キャプチャグループや遅延量化子、ストリーミングインターフェースへのサポートがあります。RE# はすでに grep スタイルの「re」ツールで使用されており、一回パスで多項語ブール検索を実行しています。RE# を採用することで、開発者はサービス拒否リスクを避けつつ POSIX の意味論を維持し、信頼できない入力に対して線形時間の性能を達成できます。
本文
検索性能と二次的爆発
あるパターンを文書内で探すのに1秒かかる場合、同じ検索を100倍大きいテキストで繰り返すとほぼ3時間まで拡大します。RE2、Go の
regexp、Rust の regex クレート、.NET の NonBacktracking モードなど、どの正規表現エンジンも「単一一致に対しては線形時間でマッチできる」と主張しています。しかし、find_iter(または FindAll)といったイテレーターを呼び出すと、その保証は消えてしまいます。Rust の正規表現ドキュメントだけが明確に次のように述べています。
「イテレーターの最悪ケース時間計算量は O(m × n²) です … パターンもヒストックも信頼できない場合、すべての一致を列挙するときには二次的な時間計算量に陥ります。これを回避する方法はありません。」
典型例として
.*a|b というパターンと n 個の b だけで構成された文字列があります。各位置でエンジンはまず .*a を試し、残りのヒストックをスキャンして a を探します(失敗)。次に b ブランチがマッチし、一文字進み再び同じことを繰り返します。計算量は( n + (n-1) + (n-2) + \dots = O(n²))。
Russ Cox は 2009 年にこれを指摘しており、BurntSushi の rebar ベンチマークは RE2、Go、Rust 全てで入力が倍増するとスループットが半減することを確認しています。
なぜ長い間見逃されていたのか?
多くの学術的正規表現論文は「単一一致問題」に焦点を当てています。理論ではすべてを「マッチする/しない」という yes/no の質問に還元します。これでは一致がどこで起きるか、何個あるか、その長さは無視されます。「マッチするかしない」だけを考えると、全一致問題は見えなくなります。
バックトラッキングエンジンはさらに悪いです:ユーザーが入力したパターンと 50 文字の入力で、宇宙の熱的死よりも長くかかる指数時間に陥ることがあります。Thompson の NFA 構築(1968)はこれを回避しましたが、バックトラッキングは多くのエンジンでデフォルトです。私の GitHub セキュリティアラート(2026 年 3 月)では、npm の glob マッチャー
minimatch が 5 件の ReDoS CVE に遭遇し、すべてバックトラッキングによるものであることが分かります。
二次的全一致問題はより微妙で、バックトラッキングを避けるように設計されたエンジンにも影響します。ブラウザをクラッシュさせるほどではありませんが、1 秒の検索を 3 時間に変える可能性があります。
歴史的解決策
Aho–Corasick(1975)
固定文字列 のみの場合、Aho–Corasick アルゴリズムは O(n) ですべての出現を見つけます。トライに失敗リンクを構築し、マッチすると正確に終点が分かるため再スキャンが不要です。ただし文字列がリテラルのみの場合に限ります。
Hyperscan / Vectorscan
これらのエンジンは earliest‑match(最初にマッチ状態に入った瞬間を報告)というセマンティクスを採用します。ネットワーク侵入検知では問題ありませんが、
grep のようなツールでは結果が変わります。例: a+ を aaaa に適用すると、一つの長いマッチではなく四つの一文字マッチが報告されます。
REmatch(VLDB 2023)
すべての有効
(start, end) スパンを列挙します。重複や入れ子も含めて、a+ を aaaa に適用すると 10 個のスパンが出力されます。そのため出力自体が O(n²) になることがあります。
RE# – 二段階線形時間全一致
RE# はパターンや入力に関係なく、すべてのマッチを二つのパスで見つける最初の正規表現エンジンです。セマンティクスは変更しません。
- 逆 DFA がマッチ開始位置となり得る場所をマークします。
- 順 DFA が各マークされた位置で最長マッチを解決します。
マッチは再起動せずに後から報告されます。アルゴリズムは任意のパターンに対して機能し、文字列がリテラルのみという制限はありません。
ハードナードモード
二段階で再起動コストを排除したものの、順方向パスではまだ一度に一つのマッチしか解決できません。曖昧な境界を持つ悪質なパターン(例:
.*a|b)はこのパス内でも二次的作業を発生させます。
ハードナードモード は順方向パスを O(n × S) の走査に置き換えます (
S = 同時にアクティブな DFA 状態数)。これで一度のパスで すべて のマッチ終点を解決し、左側最長(POSIX)セマンティクスを保ちつつ意味的トレードオフはありません。
| 入力サイズ | 通常速度 | ハードナード速度 |
|---|---|---|
| 1 000 | 0.7 ms | 25× (≈28 µs) |
| 5 000 | 18 ms | 123× (≈146 µs) |
| 10 000 | 73 ms | 241× (≈303 µs) |
| 50 000 | 1.8 s | 1.6 ms |
通常モードは二次的ですが、ハードナードは線形を保ちます。
ハードナードモードの使用タイミング
- 不正、ユーザー提供パターン:攻撃者が悪質なケースを作成できる場合。
- セキュリティクリティカル環境(例:Web サービス、ログパーサ)。
let re = Regex::with_options( pattern, EngineOptions::default().hardened(true) )?;
通常テキストでは、二次的ケースを引き起こす可能性のあるパターンに対して 3〜20 倍程度の遅延が生じます。実際には
[A-Z][a-z]+ のような境界が明確な正規表現はほとんどで、ハードナードモードを有効にしても影響はありません。
Aho–Corasick との比較
RE# のハードナードモードは、マッチ長が事前に分からない完全正規表現へ Aho–Corasick を拡張したものです。逆パスで確認済みの開始位置だけに新しい候補を追加し、アクティブなマッチ候補セットを保持して各文字で全候補を導関数(derivative)で進めます。
ベンチマーク(2 663 語の辞書 vs 約 900 KB のプローズ):
| エンジン | スループット |
|---|---|
Rust | 627 MiB/s |
| Aho–Corasick | 163 MiB/s (≈3.85× 遅い) |
| RE# 通常 | 535 MiB/s (≈1.17×速い) |
| RE# ハードナード | 155 MiB/s (≈4.05×遅い) |
RE# は派生ベースの DFA が小さく、キャッシュ効率が高いため、二段階であっても高速です。
スキップアクセラレーション & リテラル重視パターン
RE# には AVX2/NEON 用の手書き実装があります:
- レアバイト検索
- テディマルチポジションマッチング
- 文字クラススキャン(範囲ベース)
これにより、リテラル重視のベンチマーク(rebar スイート)で
regex とほぼ同等に近づきます。
ストリーミングとその他制限
| 機能 | 状態 |
|---|---|
| ストリーミングインタフェース | 未実装; はスライスを受け取ります。チャンクストリームは計画中です。 |
| キャプチャグループ | なし。RE# は境界のみ返します。キャプチャは別エンジンで後処理できます。 |
ラジー量化子() | 不可。RE# は左側最長(POSIX)セマンティクスを採用しており、ラジー量化子はバックトラッキング概念に依存します。 |
実践例: re
grep ツール
reRE# は多項語 Boolean 検索とスコーピングをサポートする簡易 grep ライクなユーティリティです。
# "unsafe" と "unwrap" の両方が含まれる行を検索 re --near 5 -a unsafe -a unwrap src/ # "serde" と "async" 両方を含むファイル一覧 re --scope file -a serde -a async src/
RE# パターンを直接使用することも可能です。
今後の展望
RE# はまだ開発中で、正式な 1.0 リリースは未定ですが進捗は続いています。PLDI に条件付きで受理された論文があります。今後はキャプチャグループ対応とハードナードモードを lookaround 向けに改善する予定です。