Regexクラスを用いた正規表現によるパターンマッチングを行う場合、Regexクラスをインスタンス化してからメソッドを呼び出す方法と、クラスメソッドを都度呼び出す方法のどちらかをとることができ、任意に選択することができます。 (正規表現によるパターンマッチングと文字列操作 §.Regexクラスのインスタンスメソッドと静的メソッド) 両者は単に呼び出し方の違いがあるだけでなく、動作上の違いもあります。 具体的には、インスタンス化する場合では正規表現を使用する前にパターンの解析が一度だけ行われます。 一方クラスメソッドの場合では、メソッドを呼び出す度にパターンの解析が行われます。 またクラスメソッドでは、最近使用されたパターンがキャッシュされます。
.NET FrameworkのRegexクラスでは、RegexOptions.Compiledを指定することにより、インスタンス作成時(実行時)に正規表現をコンパイルしてILコードを生成させることができます。 コンパイルに時間を要しますが、コンパイル済みの正規表現を繰り返し使用する場合にはパターンマッチングの実行速度を向上させることができます。
また、Regex.CompileToAssemblyメソッドを使うことにより、正規表現をプリコンパイルしてアセンブリ化することができます。 よく使う正規表現をまとめてアセンブリ化することにより正規表現ライブラリを構築することができ、またプリコンパイルにより都度実行されることによるオーバーヘッドを削減し、パフォーマンス向上を期待することができます。
ここでは、Regexクラスと正規表現のキャッシュ・コンパイルについて解説します。
正規表現のキャッシュ
.NET Frameworkの正規表現(Regexクラス)では、正規表現を使用する前に正規表現エンジンによりパターンの解析が行われます。 Regexクラスのインスタンスを作成する場合は、作成時に一度だけ解析が行われます。 クラスメソッドを使う場合では、メソッドを呼び出す度に解析が行われますが、その中でも最近使用された正規表現はキャッシュされます。 そのため、同じ正規表現を繰り返し使用する場合は、解析済みのキャッシュが再利用されるため解析は一度だけになります。
キャッシュされる正規表現の個数はデフォルトでは15個となっていて、キャッシュの個数はRegex.CacheSizeプロパティで取得・設定することが出来ます。
キャッシュによってどのような効果が表れるかを見てみると次のようになります。 次の結果は、同一の正規表現を使い、クラスメソッドとインスタンスメソッドのIsMatchをそれぞれ5×100,000回呼び出した場合の経過時間を3回計測したものです。 Regex.CacheSizeをデフォルトのままにした場合と、0
に設定してキャッシュを無効にした場合を比較すると次のようになります。
デフォルトの場合(キャッシュを有効にした場合)ではクラスメソッドの方が若干時間がかかっていますが、オーダーはどちらも同等です。 両者の差はキャッシュの有無チェックや内部で使用するインスタンスの生成などによる差と思われます。
一方、Regex.CacheSizeが0の場合(キャッシュを無効にした場合)では、クラスメソッドの処理時間が大幅に増えます。 キャッシュを無効にしたことにより、クラスメソッドの呼び出しの度にパターンの解析が行われるようになるためです。
インスタンスメソッドでは、インスタンス自身が解析済みのパターンを保持しているため、Regex.CacheSizeの変更による影響を受けません。
上記のことから次のようなことが言えると思います。
- 同じ正規表現を何度も使う場合はクラスメソッドではなくインスタンスを作成して使用することで処理時間を改善できる
- クラスメソッドを使っている状況では、
- 使用頻度が高く、かつ同じ正規表現を多く使っている場合は、Regex.CacheSizeを大きくすることで処理時間を改善できる
- 逆に、使用頻度が低いものや、複雑な正規表現を多数使っている場合は、Regex.CacheSizeを小さく(もしくは0にして無効にする)ことで使用するメモリの量を減らすことが出来る
なお、計測に使用したコードは次のとおりです。
また、Mono 3.2の時点では正規表現のキャッシュは実装されていないようで、Regex.CacheSizeの値を変更しても処理時間はほとんど変わりません。
正規表現のコンパイル (RegexOptions.Compiled)
RegexコンストラクタでRegexOptions.Compiledを指定すると、正規表現が使用される前にパターンの解析・コンパイルとILコードの生成が行われるようになります。 これにより、最初に正規表現を使用する際はコンパイルを行う分の時間はかかりますが、以降はILコードに変換されたものを使うようになるため、同じ正規表現(=Regexインスタンス)を繰り返し使用するような状況では実行速度が向上します。
次の結果は、RegexOptions.Compiledを指定したインスタンスとそうでないインスタンスの二つを作成して比較したものです。 それぞれ同じ正規表現を使い、IsMatchメソッドを5×100,000回呼び出した場合の経過時間を3回計測したものです。
この結果から分かるとおり、RegexOptions.Compiledを指定した方が、指定しなかった場合(デフォルト)と比べて処理時間が短くなっています。 正規表現の複雑さや使用頻度にもよりますが、RegexOptions.Compiledを指定することで処理時間の改善が期待できます。
なお、計測に使用したコードは次のとおりです。
参考までに、Mono 3.2の時点では正規表現のコンパイルは実装されていないようで、RegexOptions.Compiledを指定しても実行速度はほとんど変わりません。
正規表現のプリコンパイル (Regex.CompileToAssembly)
Regex.CompileToAssemblyメソッドとRegexCompilationInfoクラスを使用すると、正規表現をコンパイルしてILコードに変換し、それらをまとめたアセンブリを生成することが出来ます。 このようにしてプリコンパイルした正規表現は、生成したアセンブリを参照することで通常のRegexクラスと同様に使用することができます。
RegexOptions.Compiledでは実行時にコンパイルされるのに対し、Regex.CompileToAssemblyで生成したアセンブリを使用すれば事前にコンパイルされた正規表現を使用することになるため、実行時のコンパイル時間を削減することが出来ます。
正規表現をプリコンパイルしてアセンブリを生成する
Regex.CompileToAssemblyメソッドを使って正規表現をプリコンパイルするにはRegexCompilationInfoクラスを使ってプリコンパイルする正規表現に関する情報を指定します。
RegexCompilationInfoクラスには、コンパイルする正規表現と、その正規表現に与えるオプション(RegexOptions)を指定します。 また、コンパイル後の正規表現はRegexクラスから派生したクラスとしてアセンブリに追加されます。 このため、コンパイルされる正規表現に与えるクラス名と名前空間も同時に指定します。
次のコードでは、Regex.CompileToAssemblyとRegexCompilationInfoを使って正規表現をコンパイルし、アセンブリを生成しています。
このコードを実行すると、一つのクラスRegexLibrary.WindowsFullPathRegex
が含まれるライブラリアセンブリRegexLibrary.dll
が生成されます。 この例ではひとつのRegexCompilationInfoだけを指定しているためアセンブリにはRegex派生クラスが一つだけ含まれた状態で生成されますが、アセンブリに含めたい数に応じてRegexCompilationInfoをRegex.CompileToAssemblyメソッドに指定することにより、それらをまとめてアセンブリにすることができます。
プリコンパイルされた正規表現の使用
プリコンパイルされた正規表現を使用する場合は、生成したアセンブリを参照し、正規表現クラスのインスタンスを生成します。 Regex.CompileToAssemblyで生成されるクラスはRegexクラスの派生クラスとなっているため、通常のRegexインスタンスの操作と同様に扱うことができます。
次の例では、前項で生成したアセンブリを参照し、プリコンパイルした正規表現を使ってパターンマッチングを行っています。
このコードを実行すると、次のような結果が得られるはずです。
.NET Framework以外の実装での正規表現のプリコンパイル
Mono 3.2の時点ではRegex.CompileToAssemblyメソッドによるアセンブリの生成は実装されていません。 Regex.CompileToAssemblyメソッドを呼び出すと例外NotImplementedExceptionがスローされます。
もちろん、.NET Framework上で生成したアセンブリを参照してMonoで使用することは出来ます。