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; }
(Matches
をforeach
しようとすると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 によって正規表現利用は相当な恩恵を受けると思った。