C#において正規表現(Regex)は、文字列のパターンマッチングやバリデーションを行う上で非常に強力なツールです。
しかし、その強力さゆえに、使い方を誤るとアプリケーションのパフォーマンスに重大な影響を及ぼす「ボトルネック」となり得ます。
特に、大量のテキストデータを処理する場合や、高頻度でマッチングを繰り返すシステムでは、正規表現の最適化は避けて通れない課題です。
本記事では、C#における正規表現の高速化手法として古くから知られるRegexOptions.Compiledの効果と、近年の .NET における革命的な進化である「ソース生成器(Source Generator)」の使い分けについて、内部構造やベンチマークの観点から徹底的に解説します。
パフォーマンスを最大限に引き出すための最適な選択基準を、プロの視点で解き明かしていきましょう。

正規表現の実行モードを理解する
C#の System.Text.RegularExpressions 名前空間における正規表現には、大きく分けて3つの実行モードが存在します。
パフォーマンス最適化を検討する前に、それぞれのモードがどのように動作しているのかを理解することが重要です。
まず1つ目は、逐次解釈(Interpreted)モードです。
これはオプションを指定せずに new Regex(pattern) を呼び出した場合のデフォルトの挙動です。
実行時に正規表現パターンを解析し、独自の命令セット(オペコード)に変換して、インタープリタがそれを1つずつ読み取りながらマッチングを行います。
2つ目は、今回詳しく解説する RegexOptions.Compiled を指定した実行時コンパイルモードです。
このモードでは、正規表現パターンを解析した後、Reflection.Emit を使用して実行時に MSIL(Microsoft Intermediate Language) を直接生成します。
このMSILは、その後JIT(Just-In-Time)コンパイラによってネイティブな機械語へ変換されるため、インタープリタを介さずに高速な実行が可能となります。
3つ目は、.NET 7 以降で導入された ソース生成器(Source Generator) による最適化です。
これは、ビルド時に正規表現の解析を行い、マッチングロジックそのものをC#のソースコードとして出力する手法です。
実行時の解析コストがゼロになり、さらにコンパイラによる最適化の恩恵をフルに受けることができます。
RegexOptions.Compiled のメリットとデメリット
RegexOptions.Compiled は、長年C#エンジニアがパフォーマンス向上のために利用してきた標準的な手段です。
しかし、このオプションは「とりあえず付ければ速くなる」という魔法の杖ではありません。

高速化の仕組みと「損益分岐点」
RegexOptions.Compiled を指定すると、正規表現エンジンはパターンを最適化された実行コードへ変換します。
これにより、ループ内でのマッチング速度は劇的に向上します。
しかし、この最適化には「起動コスト」という対価が必要です。
Reflection.Emit を用いたMSILの生成と、その後のJITコンパイルには相応のCPU時間とメモリを消費します。
そのため、アプリケーション全体で数回しか使わない正規表現に対してこのオプションを適用すると、コンパイルにかかる時間が実行の短縮時間を上回ってしまい、逆効果になることがあります。
一般的に、数千回から数万回以上のマッチングを行う場合にのみ、このオプションの恩恵を十分に受けることができると言われています。
メモリ消費と動的生成の罠
もう1つの注意点はメモリ(ワーキングセット)の増大です。
コンパイルされた正規表現は、型としてアセンブリ内に保持されるため、一度生成されるとアプリケーションが終了するまでメモリを占有し続ける傾向があります。
特に注意が必要なのが、正規表現パターンを動的に生成(変数をパターンに組み込むなど)しているケースです。
動的なパターンに対して RegexOptions.Compiled を適用し続けると、実行のたびに新しいMSILが生成され、メモリリークのような状態(実際には型情報の増大)に陥り、最終的にプロセスが不安定になるリスクがあります。
.NET 7/8/9 におけるソース生成器(Source Generator)
.NET 7 で登場した GeneratedRegexAttribute は、正規表現のパフォーマンスに対する最適解を大きく変えました。
ソース生成器を利用することで、従来の RegexOptions.Compiled が抱えていた課題の多くが解決されます。

ソース生成器の使いかた
ソース生成器を利用するには、partial クラスの中に、同じく partial なメソッドを定義し、[GeneratedRegex] 属性を付与します。
using System.Text.RegularExpressions;
public partial class MyParser
{
// ビルド時にこのメソッドの実装が自動生成される
[GeneratedRegex(@"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$", RegexOptions.IgnoreCase)]
private static partial Regex EmailRegex();
public void Validate(string email)
{
if (EmailRegex().IsMatch(email))
{
// 処理
}
}
}
このコードをコンパイルすると、IDEの裏側で非常に高度に最適化されたC#コードが生成されます。
開発者はそのコードをデバッグすることも可能ですし、何より「実行時のコンパイルコストをゼロにしながら、最高速の実行速度を得る」ことができます。
ソース生成器が優れている理由
ソース生成器には、従来の RegexOptions.Compiled にはなかった以下のメリットがあります。
- 起動の高速化
実行時にMSILを生成する必要がないため、アプリケーションの起動や最初のマッチングが非常にスムーズです。
- Native AOT との親和性
実行時のコード生成(Reflection.Emit)を行わないため、Native AOT 環境でも制限なく動作します。
- トリミングの最適化
不要なコードを削除するトリミング機能において、どの正規表現が使われているかがビルド時に確定しているため、バイナリサイズを小さく保てます。
- 高度な最適化エンジン
最新の .NET エンジンは、パターンを分析して非バックトラック型のエンジン(NonBacktracking)を自動選択するなど、手動では困難なレベルの最適化を施します。
パフォーマンス比較:どのくらい速くなるのか?
実際にどの程度の差が出るのか、一般的なベンチマークの傾向を元に比較してみましょう。
以下の表は、ある複雑なテキスト解析を 100,000回 繰り返した場合の相対的なパフォーマンスを示したイメージです。
| モード | 起動時間(初回実行) | 実行速度(スループット) | メモリ使用量 | 備考 |
|---|---|---|---|---|
| デフォルト (Interpreted) | 最速 | 遅い | 低い | 開発・デバッグ用 |
| RegexOptions.Compiled | 非常に遅い | 非常に速い | 高い | 大規模ループ向け |
| ソース生成器 | 最速 | 最高速 | 最適化済み | 推奨される選択肢 |

多くのケースにおいて、ソース生成器は RegexOptions.Compiled と同等、あるいはそれ以上の実行速度を叩き出しつつ、起動コストを完全に排除しています。
これは、ビルド時にパターンを静的に解析することで、個別の文字マッチングに対してインライン化やベクトル化(SIMD)の最適化を適用できるためです。
実践的な使い分けガイドライン
これまでの内容を踏まえ、現代のC#開発における正規表現の使い分け基準をまとめます。
基本的には「ソース生成器を第一選択」としつつ、状況に応じて他の選択肢を検討するのがプロの作法です。
1. ソース生成器を使用すべきケース(第一選択)
正規表現パターンがコンパイル時に確定している場合は、迷わずソース生成器を選択してください。
- 入力バリデーション(メールアドレス、電話番号など)
- 固定されたログフォーマットの解析
- 静的なキーワード抽出
特に、クラウドネイティブな環境(AWS Lambda や Azure Functions など)では、コールドスタート時のパフォーマンスが重要になるため、ソース生成器の恩恵は計り知れません。
2. RegexOptions.Compiled を検討するケース
ソース生成器が利用できない、あるいは適さない特殊な環境では、依然として有効な選択肢となります。
- .NET Standard 2.0 以前 や、古い .NET Framework をターゲットにしているライブラリ開発。
- 動的に生成されるが、一度生成したら数百万回使い回すパターン。
- ソース生成器を導入するために partial クラス化するなどのリファクタリングが困難なレガシーコードの改修。
ただし、前述の通り動的生成によるメモリ増大のリスクは常に意識する必要があります。
3. デフォルト(逐次解釈)で十分なケース
すべての正規表現を高速化する必要はありません。
- アプリケーションのライフサイクル全体で数回しか実行されないマッチング。
- パフォーマンスが全くクリティカルではない補助的な処理。
- ユーザーが画面から自由に入力した検索パターンを実行する場合(この場合、事前にコンパイルするコストが無駄になります)。
高速化のための高度なテクニック
正規表現の高速化は、実行モードの選択だけではありません。
パターン設計そのものを見直すことで、さらに次元の違う速度を手に入れることができます。

非バックトラックエンジンの活用
.NET 7 以降では、RegexOptions.NonBacktracking というオプションが追加されました。
これは、入力の長さに対して線形時間(O(n))で実行を完了することを保証するエンジンです。
従来のエンジンは、マッチングに失敗した際に「戻って別の可能性を試す」というバックトラックを行いますが、これが特定の入力に対して指数関数的な時間を要する(正規表現DoS攻撃の原因にもなる)問題がありました。
NonBacktracking オプションは、実行速度の安定性を極めて高く保つことができます。
タイムアウトの設定
高速化とは直接関係ありませんが、パフォーマンスの「安定性」を確保するために、必ずタイムアウト(TimeSpan)を設定する習慣をつけましょう。
[GeneratedRegex(@"patter", RegexOptions.None, matchTimeoutMilliseconds: 100)]
万が一、想定外に重いパターンや入力が与えられた場合でも、システム全体がフリーズするのを防ぐことができます。
これはプロフェッショナルなコードにおいて必須の作法です。
Span<T> との連携
最新の .NET では、文字列の切り出し(Substring)によるメモリ割当を避けるために ReadOnlySpan<char> を活用することが推奨されます。
Regex クラスも Span を受け取るメソッドをサポートしており、これを活用することで GC(ガベージコレクション)の負荷を抑えた超高速なマッチングが可能になります。
まとめ
C#における正規表現の最適化は、かつての RegexOptions.Compiled 一択の時代から、ソース生成器(Source Generator)を核とした新しい時代へとシフトしました。
最後に、重要なポイントを振り返りましょう。
- デフォルト
低頻度の実行やパターン解析コストを嫌う場合に適しています。
- RegexOptions.Compiled
古い .NET 環境や動的パターンの大量繰り返しに有効ですが、起動コストとメモリ増大に注意が必要です。
- ソース生成器
モダンな .NET 開発における「銀の弾丸」です。
ビルド時に最適化を行うため、起動・実行ともに最高レベルのパフォーマンスを発揮します。
- NonBacktracking, Span, タイムアウトの設定
NonBacktrackingやSpanの活用、そしてタイムアウトの設定を組み合わせることで、堅牢で高速な文字列処理基盤を構築できます。
C# 開発者として、これらの技術を適切に使い分けることは、単にコードを速くするだけでなく、リソース効率に優れたプロフェッショナルなアプリケーションを生み出すことに繋がります。
ぜひ、あなたのプロジェクトでもソース生成器の導入から始めてみてください。
