てくメモ

trivial な notes

【C#】GeneratedRegex が速すぎる

Youtubeの動画みたいな記事タイトル。でも速い。

GeneratedRegex .NET 7.0 から入ったソースジェネレーターによる正規表現


今回は、ある単語を含む行を抽出することを考えてみる。

まずは正規表現ではなく、StreamReaderを用いたもの。

// StreamReader reader;
public List<string> ByStreamReader()
{
    List<string> list = new(11);

    for(var line = reader.ReadLine(); line is not null; line = reader.ReadLine())
    {
        if (line.Contains("イーハトーヴォ")) list.Add(line);
    }

    return list;
}


これを正規表現で置き換えてみる。
比較として .NET 7.0 以前としてまず考えてみる。

public List<string> ByRegularRegex()
{
    List<string> list = new(11);

    var text = reader.ReadToEnd();
    var regex = new Regex(".*イーハトーヴォ[^\r\n]*", RegexOptions.Compiled);

    foreach(var match in regex.Matches(text).Cast<Match>())
    {
        list.Add(match.Value);
    }

    return list;
}

Matchesforeachしようとするとobjectを渡されて最初「え?」となるのは C# 正規表現あるある。ジェネリックがなかった時代の名残ということ。Cast<Match>()するなりIReadOnlyCollection<Match>でキャストするなり)


そして GeneratedRegex。
利用するのはとても簡単で、特に VisualStudio なら従来型に書いてクイックアクションで変換可能。

public List<string> ByGenRegex()
{
    List<string> list = new(11);

    var buf = ArrayPool<char>.Shared.Rent((int)stream.Length);
    try
    {
        Span<char> span = buf;
        reader.Read(span);

        Regex regex = MyRegex();
        foreach (var m in regex.EnumerateMatches(span))
        {
            list.Add(new(span.Slice(m.Index, m.Length)));
        }

        return list;
    }
    finally
    {
        ArrayPool<char>.Shared.Return(buf);
    }
}

[GeneratedRegex(".*イーハトーヴォ[^\r\n]*")]
private static partial Regex MyRegex();

ReadOnlySpan<char>を受けるAPIを使うので比較のうちとしてArrayPool<char>利用


これらを BenchmarkDotNet で比較。
対象テキストファイルはイーハトーヴォこと「ポラーノの広場」。

上記まで以外の比較用コード折りたたみ

// 青空文庫 宮沢賢治「ポラーノの広場」(Shift-JIS)
private const string path =
#if DEBUG
    "poranono_hiroba.txt";
#else
    "../../../../poranono_hiroba.txt";
#endif
private Encoding encoding = Encoding.GetEncoding("shift_jis");
// Shift-JIS のためにコンストラクタで RegisterProvider
static ReadStringBenchmark() => Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

private FileStream stream = default!;
private StreamReader reader = default!;

[IterationSetup]
public void ItrSetup()
{
    stream = File.OpenRead(path);
    reader = new StreamReader(stream, encoding);
}

[IterationCleanup]
public void ItrCleanup()
{
    reader.Dispose();
    stream.Dispose();
}

         Method |       Mean |      Error |    StdDev | Allocated |
--------------- |-----------:|-----------:|----------:|----------:|
 ByStreamReader |   379.0 μs |   109.2 μs |   5.99 μs | 156.07 KB |
 ByRegularRegex | 2,833.2 μs | 1,999.0 μs | 109.57 μs | 215.83 KB |
     ByGenRegex |   398.9 μs |   129.8 μs |   7.12 μs |   8.69 KB |


従来型の正規表現ではかなりビハインドなのに対して、GeneratedRegex 版は処理そのものを書いたStreamReader版に対して速度が遜色ない。

今回は実質string.Containsと同じ働きしかしていないけれど、もちろん正規表現はもっとリッチに書けるわけで、GeneratedRegex によって正規表現利用は相当な恩恵を受けると思った。