.NET 7 (Preview 2) 新機能 : Regex Source Generator

2022年3月14日、.NET 7 Preview 2 が公開されました。.NET 7 にはいくつもの新機能がありますが、今回はその中でRegex Source Generator について触ってみた記事になります。 (日本語での正式名称は未確認です。正規表現ソースジェネレータ?)

※Preview 2 を少しいじった見解です。今後変更の可能性はありますし、間違ったことを書いている可能性があります。

これまでの正規表現

これまでは、以下のように正規表現オブジェクトを作成していました。

public class Sample
{
    public static readonly Regex _regex = new Regex(@"^0d{2,4}-d{2,4}-d{4}$", RegexOptions.Compiled);

    public bool IsMatch(string input)
    {
        return _regex.IsMatch(input);
    }
}

staticRegexOptions.Compiledの有無などは用途によって選択していたと思います。あらかじめパターンがわかっているのなら基本的にはstaticかつRegexOptions.Compiledで実装し、初期化時間や正規表現の評価が行われる回数によって調整していました。

つまり、「起動に時間をかけて実行速度を重視するか」、「起動を重視して実行速度に少し目を瞑るか」というトーレドオフの関係にありました。

Regex Source Generator

そこで今回の Regex Source Generator の登場です。前述の「起動に時間をかけて実行速度を重視するか」、「起動速度を重視して実行速度に少し目を瞑るか」という問いに対して「起動速度も実行速度もどちらも重視する」という強欲な回答になります。

Regex Source Generator の使い方は以下の通りです。

public partial class Sample
{
    [RegexGenerator(@"^0d{2,4}-d{2,4}-d{4}$", RegexOptions.IgnoreCase)]
    public static partial Regex Regex();

    public bool IsMatch(string input)
    {
        return Regex().IsMatch(input);
    }
}

コード上の違いをざっくりまとめます。

  • クラスをpartialにする。
  • フィールドではなくpartialなメソッドにする。
  • RegexGenerator属性で正規表現を指定する。

こちらの記事やクラスやメソッドをpartialで指定することから、Regex Source Generator を使うことでコンパイラがコンパイル済の正規表現エンジンをメソッドとして埋め込んでくれるということだと思われます。ビルドしてみたところ RegexGenerator.g.csというファイルが自動生成されており、正規表現エンジンの実装が確認できました。

ベンチマーク

実際どの程度違うのか検証するため、以下のようなコードでベンチマークをとってみました。

public class Test
{
    [Benchmark]
    public void IsMatch()
    {
        var sample = new Sample();
        sample.IsMatch("080-1234-5678");
    }

    [Benchmark]
    public void IsMatch_10()
    {
        for (var i = 0; i < 10; i++)
        {
            var sample = new Sample();
            sample.IsMatch("080-1234-5678");
        }
    }
}

実行時間

正規表現MethodMeanErrorStdDev
staticでRegexOptions.CompiledなしIsMatch
IsMatch_10
99.94
1,053.92
2.014
24.483
5.681
70.640
staticでRegexOptions.CompiledありIsMatch
IsMatch_10
44.65
447.42
0.920
8.908
2.502
15.126
Regex Source GeneratorIsMatch
IsMatch_10
41.54
447.94
0.774
7.724
1.182
6.450
(単位 : ns)

初期化時間 (ここだけ計測するコードを書いて計測)

正規表現Mean
staticでRegexOptions.Compiledなし35.804
staticでRegexOptions.Compiledあり11.675
Regex Source Generator0.626
(単位 : ms)

ベンチマーク結果からわかるように、圧倒的に起動時間が早いです。実行速度はRegexOptions.Compiledの時と同等といって良いでしょう。起動速度と実行速度のいいとこ取りができていると言えます。これまでの正規表現のRegexOptions.Compiledあり/なしと、Regex Source Generator の速度の関係は以下の通りです。

速度の他に注意するべき点として、Regex Source Generator はコンパイルされたdllのファイルサイズが大きくなることが挙げられます。ベンチマークのプロジェクトでは、これまでの正規表現では 5.6KB だったものが Regex Source Generator では 7.6KB となり、正規表現1つの違いで約 2KB 増加しました。僅かな差ではありますが、実行環境によっては影響があることもあるため、気に留めておいた方が良さそうです。

既存コードのリファクタリング

これだけの性能を見せられると、すべての正規表現を Regex Source Generator に差し替えたくなります。しかし、これまでの正規表現を Regex Source Generator に差し替える場合、プロパティやフィールドからメソッドに差し替える必要があるため少し困ることもありそうです。Visual Studio のリファクタリングでは、名前の変更で ()を入力できませんし、外部の dll から参照されていた場合はそちらも変更しなければなりません。

そこで以下のようなリファクタリングを試してみました。

public partial class Sample
{
    //public static Regex PhoneNumber { get; } = new Regex(@"^0d{2,4}-d{2,4}-d{4}$", RegexOptions.IgnoreCase);
    
    [RegexGenerator(@"^0d{2,4}-d{2,4}-d{4}$", RegexOptions.IgnoreCase)]
    public static partial Regex GeneratePhoneNumberRegex();
    public static Regex PhoneNumber { get; } = GeneratePhoneNumberRegex();

    public bool IsMatch(string input)
    {
        return PhoneNumber.IsMatch(input);
    }
}

Regex オブジェクトのnewを Regex Source Generator のコールに差し替えただけです。Regex Source Generatorの戻り値が Regex オブジェクトになっているため等価になっているはずです。既存のプロパティはそのまま存在しているため、参照している箇所の変更は必要ありません。このコードでベンチマークを取った結果、期待通り初期化も実行もRegex Source Generatorと同等の速度でした。

まとめ

余程特殊な環境でない限り、.NET 7 からの正規表現は Regex Source Generator 一択といっても過言ではないと思われます。リファクタリングはプロパティを差し替える方法が良さそうです (多分)。

コメントを残す

メールアドレスが公開されることはありません。

CAPTCHA