Regexクラスを用いた正規表現によるパターンマッチングを行う場合、Regexクラスをインスタンス化してからメソッドを呼び出す方法と、クラスメソッドを都度呼び出す方法のどちらかをとることができ、任意に選択することができます。 (正規表現によるパターンマッチングと文字列操作 §.Regexクラスのインスタンスメソッドと静的メソッド) 両者は単に呼び出し方の違いがあるだけでなく、動作上の違いもあります。 具体的には、インスタンス化する場合では正規表現を使用する前にパターンの解析が一度だけ行われます。 一方クラスメソッドの場合では、メソッドを呼び出す度にパターンの解析が行われます。 またクラスメソッドでは、最近使用されたパターンがキャッシュされます。

.NET FrameworkのRegexクラスでは、RegexOptions.Compiledを指定することにより、インスタンス作成時(実行時)に正規表現をコンパイルしてILコードを生成させることができます。 コンパイルに時間を要しますが、コンパイル済みの正規表現を繰り返し使用する場合にはパターンマッチングの実行速度を向上させることができます。

また、Regex.CompileToAssemblyメソッドを使うことにより、正規表現をプリコンパイルしてアセンブリ化することができます。 よく使う正規表現をまとめてアセンブリ化することにより正規表現ライブラリを構築することができ、またプリコンパイルにより都度実行されることによるオーバーヘッドを削減し、パフォーマンス向上を期待することができます。

ここでは、Regexクラスと正規表現のキャッシュ・コンパイルについて解説します。

§1 正規表現のキャッシュ

.NET Frameworkの正規表現(Regexクラス)では、正規表現を使用する前に正規表現エンジンによりパターンの解析が行われます。 Regexクラスのインスタンスを作成する場合は、作成時に一度だけ解析が行われます。 クラスメソッドを使う場合では、メソッドを呼び出す度に解析が行われますが、その中でも最近使用された正規表現はキャッシュされます。 そのため、同じ正規表現を繰り返し使用する場合は、解析済みのキャッシュが再利用されるため解析は一度だけになります。

キャッシュされる正規表現の個数はデフォルトでは15個となっていて、キャッシュの個数はRegex.CacheSizeプロパティで取得・設定することが出来ます。

キャッシュによってどのような効果が表れるかを見てみると次のようになります。 次の結果は、同一の正規表現を使い、クラスメソッドとインスタンスメソッドのIsMatchをそれぞれ5×100,000回呼び出した場合の経過時間を3回計測したものです。 Regex.CacheSizeをデフォルトのままにした場合と、0に設定してキャッシュを無効にした場合を比較すると次のようになります。

Regex.CacheSizeがデフォルトの場合
Microsoft Windows NT 5.1.2600 Service Pack 3
2.0.50727.3615
Regex.CacheSize = 15
[0]
static:   00:00:00.9203100
instance: 00:00:00.5604796
[1]
static:   00:00:00.9829842
instance: 00:00:00.5675445
[2]
static:   00:00:00.9279713
instance: 00:00:00.5522659
Regex.CacheSizeに0を指定してキャッシュを無効にした場合
Microsoft Windows NT 5.1.2600 Service Pack 3
2.0.50727.3615
Regex.CacheSize = 0
[0]
static:   00:00:06.7950112
instance: 00:00:00.5796448
[1]
static:   00:00:06.6722204
instance: 00:00:00.5643808
[2]
static:   00:00:06.9712903
instance: 00:00:00.5722832

デフォルトの場合(キャッシュを有効にした場合)ではクラスメソッドの方が若干時間がかかっていますが、オーダーはどちらも同等です。 両者の差はキャッシュの有無チェックや内部で使用するインスタンスの生成などによる差と思われます。

一方、Regex.CacheSizeが0の場合(キャッシュを無効にした場合)では、クラスメソッドの処理時間が大幅に増えます。 キャッシュを無効にしたことにより、クラスメソッドの呼び出しの度にパターンの解析が行われるようになるためです。

インスタンスメソッドでは、インスタンス自身が解析済みのパターンを保持しているため、Regex.CacheSizeの変更による影響を受けません。

上記のことから次のようなことが言えると思います。

  1. 同じ正規表現を何度も使う場合はクラスメソッドではなくインスタンスを作成して使用することで処理時間を改善できる
  2. クラスメソッドを使っている状況では、
    1. 使用頻度が高く、かつ同じ正規表現を多く使っている場合は、Regex.CacheSizeを大きくすることで処理時間を改善できる
    2. 逆に、使用頻度が低いものや、複雑な正規表現を多数使っている場合は、Regex.CacheSizeを小さく(もしくは0にして無効にする)ことで使用するメモリの量を減らすことが出来る

なお、計測に使用したコードは次のとおりです。

using System;
using System.Diagnostics;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    string[] files = new string[] {
      @"xcopy C:\test D:\target\backup\files",
      @"C:\Windows\Microsoft.NET\Framework\v3.5",
      @"http://example.com/",
      @"/usr/bin/mono",
      @"cd ..\..\backup\",
    };

    const string pattern = @"([A-Z]:)(\\[^\\ ]+)+"; // 使用する正規表現のパターン
    const int repeat = 100 * 1000; // 繰り返しの回数

    Console.WriteLine(Environment.OSVersion);
    Console.WriteLine(Environment.Version);

    // Regex.CacheSize = 0;
    Console.WriteLine("Regex.CacheSize = {0}", Regex.CacheSize);

    for (int t = 0; t < 3; t++) {
      // クラスメソッドを使った場合の経過時間を計測
      Stopwatch sw1 = Stopwatch.StartNew();

      for (int i = 0; i < repeat; i++) {
        foreach (string file in files) {
          Regex.IsMatch(file, pattern);
        }
      }

      sw1.Stop();

      // インスタンスメソッドを使った場合の経過時間(インスタンス生成時間を含む)を計測
      Stopwatch sw2 = Stopwatch.StartNew();

      Regex regex = new Regex(pattern);

      for (int i = 0; i < repeat; i++) {
        foreach (string file in files) {
          regex.IsMatch(file);
        }
      }

      sw2.Stop();

      Console.WriteLine("[{0}]", t);
      Console.WriteLine("static:   {0}", sw1.Elapsed);
      Console.WriteLine("instance: {0}", sw2.Elapsed);
    }
  }
}

また、Mono 3.2の時点では正規表現のキャッシュは実装されていないようで、Regex.CacheSizeの値を変更しても処理時間はほとんど変わりません。

Mono 3.2での実行結果
Unix 3.2.0.55
4.0.30319.17020
Regex.CacheSize = 15
[0]
static:   00:00:01.1630656
instance: 00:00:00.8731037
[1]
static:   00:00:01.1064189
instance: 00:00:00.8609995
[2]
static:   00:00:01.1116817
instance: 00:00:00.8676544



Unix 3.2.0.55
4.0.30319.17020
Regex.CacheSize = 0
[0]
static:   00:00:01.1920400
instance: 00:00:00.8746201
[1]
static:   00:00:01.1171539
instance: 00:00:00.8717106
[2]
static:   00:00:01.1168534
instance: 00:00:00.8748114


§2 正規表現のコンパイル (RegexOptions.Compiled)

RegexコンストラクタでRegexOptions.Compiledを指定すると、正規表現が使用される前にパターンの解析・コンパイルとILコードの生成が行われるようになります。 これにより、最初に正規表現を使用する際はコンパイルを行う分の時間はかかりますが、以降はILコードに変換されたものを使うようになるため、同じ正規表現(=Regexインスタンス)を繰り返し使用するような状況では実行速度が向上します。

次の結果は、RegexOptions.Compiledを指定したインスタンスとそうでないインスタンスの二つを作成して比較したものです。 それぞれ同じ正規表現を使い、IsMatchメソッドを5×100,000回呼び出した場合の経過時間を3回計測したものです。

RegexOptions.Compiledの指定の有無による経過時間の差異
[0]
default:  00:00:00.5575484
compiled: 00:00:00.3660707
[1]
default:  00:00:00.6100348
compiled: 00:00:00.3872334
[2]
default:  00:00:00.5817512
compiled: 00:00:00.3733582

この結果から分かるとおり、RegexOptions.Compiledを指定した方が、指定しなかった場合(デフォルト)と比べて処理時間が短くなっています。 正規表現の複雑さや使用頻度にもよりますが、RegexOptions.Compiledを指定することで処理時間の改善が期待できます。

なお、計測に使用したコードは次のとおりです。

using System;
using System.Diagnostics;
using System.Text.RegularExpressions;

class Sample {
  static void Main()
  {
    string[] files = new string[] {
      @"xcopy C:\test D:\target\backup\files",
      @"C:\Windows\Microsoft.NET\Framework\v3.5",
      @"http://example.com/",
      @"/usr/bin/mono",
      @"cd ..\..\backup\",
    };

    const string pattern = @"([A-Z]:)(\\[^\\ ]+)+"; // 使用する正規表現のパターン
    const int repeat = 100 * 1000; // 繰り返しの回数

    Console.WriteLine(Environment.OSVersion);
    Console.WriteLine(Environment.Version);

    for (int t = 0; t < 3; t++) {
      // オプションを指定していないインスタンスを使った場合の経過時間(インスタンス生成時間を含む)を計測
      Stopwatch sw1 = Stopwatch.StartNew();

      Regex regexDefault = new Regex(pattern);

      for (int i = 0; i < repeat; i++) {
        foreach (string file in files) {
          regexDefault.IsMatch(file);
        }
      }

      sw1.Stop();

      // RegexOptions.Compiledを指定したインスタンスを使った場合の経過時間(インスタンス生成時間を含む)を計測
      Stopwatch sw2 = Stopwatch.StartNew();

      Regex regexCompiled = new Regex(pattern, RegexOptions.Compiled);

      for (int i = 0; i < repeat; i++) {
        foreach (string file in files) {
          regexCompiled.IsMatch(file);
        }
      }

      sw2.Stop();

      Console.WriteLine("[{0}]", t);
      Console.WriteLine("default:  {0}", sw1.Elapsed);
      Console.WriteLine("compiled: {0}", sw2.Elapsed);
    }
  }
}

参考までに、Mono 3.2の時点では正規表現のコンパイルは実装されていないようで、RegexOptions.Compiledを指定しても実行速度はほとんど変わりません。

Mono 3.2での実行結果
Unix 3.2.0.55
4.0.30319.17020
[0]
default:  00:00:00.9098989
compiled: 00:00:00.8690936
[1]
default:  00:00:00.8714453
compiled: 00:00:00.8714634
[2]
default:  00:00:00.8824344
compiled: 00:00:00.9187251

§3 正規表現のプリコンパイル (Regex.CompileToAssembly)

Regex.CompileToAssemblyメソッドRegexCompilationInfoクラスを使用すると、正規表現をコンパイルしてILコードに変換し、それらをまとめたアセンブリを生成することが出来ます。 このようにしてプリコンパイルした正規表現は、生成したアセンブリを参照することで通常のRegexクラスと同様に使用することができます。

RegexOptions.Compiledでは実行時にコンパイルされるのに対し、Regex.CompileToAssemblyで生成したアセンブリを使用すれば事前にコンパイルされた正規表現を使用することになるため、実行時のコンパイル時間を削減することが出来ます。

§3.1 正規表現をプリコンパイルしてアセンブリを生成する

Regex.CompileToAssemblyメソッドを使って正規表現をプリコンパイルするにはRegexCompilationInfoクラスを使ってプリコンパイルする正規表現に関する情報を指定します。

RegexCompilationInfoクラスには、コンパイルする正規表現と、その正規表現に与えるオプション(RegexOptions)を指定します。 また、コンパイル後の正規表現はRegexクラスから派生したクラスとしてアセンブリに追加されます。 このため、コンパイルされる正規表現に与えるクラス名と名前空間も同時に指定します。

次のコードでは、Regex.CompileToAssemblyとRegexCompilationInfoを使って正規表現をコンパイルし、アセンブリを生成しています。

プリコンパイルされた正規表現を含むアセンブリを生成するコード
using System;
using System.Text.RegularExpressions;
using System.Reflection;

class Sample {
  static void Main()
  {
    const string pattern = @"([A-Z]:)(\\[^\\ ]+)+";

    // コンパイルする正規表現に関する情報を作成
    var rci = new RegexCompilationInfo(pattern,                // コンパイルする正規表現
                                       RegexOptions.None,      // 正規表現に指定するRegexOptions
                                       "WindowsFullPathRegex", // コンパイル後の正規表現に与えるクラス名
                                       "RegexLibrary",         // コンパイル後のクラスの名前空間
                                       true);                  // クラスをpublicにする

    // 生成されるアセンブリのAssemblyName
    var assmName = new AssemblyName("RegexLibrary");

    // コンパイルしてアセンブリを生成する
    Regex.CompileToAssembly(new[] {rci}, assmName);
  }
}

このコードを実行すると、一つのクラスRegexLibrary.WindowsFullPathRegexが含まれるライブラリアセンブリRegexLibrary.dllが生成されます。 この例ではひとつのRegexCompilationInfoだけを指定しているためアセンブリにはRegex派生クラスが一つだけ含まれた状態で生成されますが、アセンブリに含めたい数に応じてRegexCompilationInfoをRegex.CompileToAssemblyメソッドに指定することにより、それらをまとめてアセンブリにすることができます。

§3.2 プリコンパイルされた正規表現の使用

プリコンパイルされた正規表現を使用する場合は、生成したアセンブリを参照し、正規表現クラスのインスタンスを生成します。 Regex.CompileToAssemblyで生成されるクラスはRegexクラスの派生クラスとなっているため、通常のRegexインスタンスの操作と同様に扱うことができます。

次の例では、前項で生成したアセンブリを参照し、プリコンパイルした正規表現を使ってパターンマッチングを行っています。

プリコンパイルされた正規表現を含むアセンブリを参照して使用するコード
// csc test.cs /r:RegexLibrary.dll
// test.exe

using System;
using System.Text.RegularExpressions;
using RegexLibrary; // RegexCompilationInfoで指定した名前空間

class Sample {
  static void Main()
  {
    var files = new string[] {
      @"xcopy C:\test D:\target\backup\files",
      @"C:\Windows\Microsoft.NET\Framework\v3.5",
      @"http://example.com/",
      @"/usr/bin/mono",
      @"cd ..\..\backup\",
    };

    var r = new WindowsFullPathRegex(); // RegexCompilationInfoで指定したクラス名

    Console.WriteLine("pattern: {0}", r.ToString());

    foreach (var file in files) {
      Console.Write("{0,-45} ", file);

      if (r.IsMatch(file))
        Console.WriteLine("O");
      else
        Console.WriteLine("X");
    }
  }
}

このコードを実行すると、次のような結果が得られるはずです。

実行結果
pattern: ([A-Z]:)(\\[^\\ ]+)+
xcopy C:\test D:\target\backup\files          O
C:\Windows\Microsoft.NET\Framework\v3.5       O
http://example.com/                           X
/usr/bin/mono                                 X
cd ..\..\backup\                              X

§3.3 .NET Framework以外の実装での正規表現のプリコンパイル

Mono 3.2の時点ではRegex.CompileToAssemblyメソッドによるアセンブリの生成は実装されていません。 Regex.CompileToAssemblyメソッドを呼び出すと例外NotImplementedExceptionがスローされます。

Mono 3.2でRegex.CompileToAssemblyメソッドを呼び出した場合
$ gmcs compile.cs && mono compile.exe

Unhandled Exception: System.NotImplementedException: The requested feature is not implemented.
  at System.Text.RegularExpressions.Regex.CompileToAssembly (System.Text.RegularExpressions.RegexCompilationInfo[] regexes, System.Reflection.AssemblyName aname, System.Reflection.Emit.CustomAttributeBuilder[] attribs, System.String resourceFile) [0x00000] in <filename unknown>:0 
  at System.Text.RegularExpressions.Regex.CompileToAssembly (System.Text.RegularExpressions.RegexCompilationInfo[] regexes, System.Reflection.AssemblyName aname) [0x00000] in <filename unknown>:0 
  at Sample.Main () [0x00000] in <filename unknown>:0 

もちろん、.NET Framework上で生成したアセンブリを参照してMonoで使用することは出来ます。

.NET Framework上のRegex.CompileToAssemblyで生成したアセンブリをMonoで参照する
$ gmcs test.cs /r:RegexLibrary.dll && mono test.exe
pattern: ([A-Z]:)(\\[^\\ ]+)+
xcopy C:\test D:\target\backup\files          O
C:\Windows\Microsoft.NET\Framework\v3.5       O
http://example.com/                           X
/usr/bin/mono                                 X
cd ..\..\backup\                              X