|
| 1 | +--- |
| 2 | +title: Benchmark regex |
| 3 | +date: 2026-02-26 07:44:00 +0100 |
| 4 | +categories: [dotnet, benchmark] |
| 5 | +tags: [dotnet, benchmark, regex, regular expressions] # TAG names should always be lowercase |
| 6 | +--- |
| 7 | + |
| 8 | +Regular expressions (regex) are patterns used to match character combinations in strings. In .NET, there are several ways to use regex for matching. The simplest form is: |
| 9 | + |
| 10 | +```csharp |
| 11 | +System.Text.RegularExpressions.Regex.IsMatch("my text", "[a]"); |
| 12 | +``` |
| 13 | + |
| 14 | +It's important to note that using `Regex.IsMatch(input, pattern)` directly is not performant and should generally be avoided. A slightly better approach is to create an instance of the `Regex` class with the pattern: |
| 15 | + |
| 16 | +```csharp |
| 17 | +var pattern = new Regex("[a]"); |
| 18 | +pattern.IsMatch("my text"); |
| 19 | +``` |
| 20 | + |
| 21 | +This approach caches the pattern, but it can still be slow for repeated use. To improve performance, you can compile the pattern: |
| 22 | + |
| 23 | +```csharp |
| 24 | +var pattern = new Regex("[a]", RegexOptions.Compiled); |
| 25 | +pattern.IsMatch("my text"); |
| 26 | +``` |
| 27 | +With `RegexOptions.Compiled`, the compiler generates dynamic code to speed up matching. This reduces the overhead of interpreting the regex pattern at runtime, resulting in faster execution, especially for patterns used frequently. |
| 28 | + |
| 29 | +For a long time, this was the fastest way to use regular expressions in .NET. However, since .NET 7, a fourth option was introduced: source-generated regex using a source generator. You can use it as follows: |
| 30 | + |
| 31 | +```csharp |
| 32 | +partial class MyClass |
| 33 | +{ |
| 34 | + public static void MyMethod() |
| 35 | + { |
| 36 | + var pattern = MyRegex(); |
| 37 | + pattern.IsMatch("my text"); |
| 38 | + } |
| 39 | + |
| 40 | + [GeneratedRegex("[a]", RegexOptions.Compiled)] |
| 41 | + private static partial Regex MyRegex(); |
| 42 | +} |
| 43 | +``` |
| 44 | + |
| 45 | +The generated code can be found under Dependencies → Analyzers → System.Text.RegularExpressions. The source generator essentially creates the same code as the compiled approach, but at compile time, so there is no runtime overhead. This method also benefits ahead-of-time (AOT) compilation in .NET 10 and later, since it avoids generating dynamic code at runtime. |
| 46 | + |
| 47 | +## The benchmark |
| 48 | + |
| 49 | +The benchmarks were created using [BenchmarkDotNet](https://www.nuget.org/packages/BenchmarkDotNet) (version 0.15.8). |
| 50 | + |
| 51 | +### Benchmark code |
| 52 | + |
| 53 | +```csharp |
| 54 | +using BenchmarkDotNet.Attributes; |
| 55 | +using System.Text.RegularExpressions; |
| 56 | + |
| 57 | +_ = BenchmarkDotNet.Running.BenchmarkRunner.Run<RegexBenchmark>(); |
| 58 | + |
| 59 | +public partial class RegexBenchmark |
| 60 | +{ |
| 61 | + [Params( |
| 62 | + "first.lastname@outlook.com" |
| 63 | + , "invalid-email" |
| 64 | + , "another.invalid-email@" |
| 65 | + , "test@" |
| 66 | + , "user@domain" |
| 67 | + , "a@b.c" |
| 68 | + , "very.long.email.address.with.many.dots@subdomain.example.com" |
| 69 | + )] |
| 70 | + public string input; |
| 71 | + |
| 72 | + private readonly string pattern = "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"; |
| 73 | + private readonly Regex regex = new("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"); |
| 74 | + private readonly Regex regexCompiled = new("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$", RegexOptions.Compiled); |
| 75 | + |
| 76 | + [Benchmark] |
| 77 | + public bool RegexWithStringPattern() => Regex.IsMatch(input, pattern); |
| 78 | + |
| 79 | + [Benchmark] |
| 80 | + public bool RegexWithNewRegexString() => regex.IsMatch(input); |
| 81 | + |
| 82 | + [Benchmark(Baseline = true)] |
| 83 | + public bool RegexWithNewRegexStringCompiled() => regexCompiled.IsMatch(input); |
| 84 | + |
| 85 | + [Benchmark] |
| 86 | + public bool RegexWithStaticPartialMethod() => EmailPartialRegex().IsMatch(input); |
| 87 | + |
| 88 | + [GeneratedRegex("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")] |
| 89 | + private static partial Regex EmailPartialRegex(); |
| 90 | +} |
| 91 | +``` |
| 92 | + |
| 93 | +### Overall benchmark results |
| 94 | + |
| 95 | +| Method | input | Mean | Error | StdDev | Ratio | RatioSD | |
| 96 | +|-------------------------------- |--------------------- |----------:|---------:|---------:|------:|--------:| |
| 97 | +| RegexWithStringPattern | a@b.c | 142.86 ns | 1.514 ns | 1.416 ns | 3.10 | 0.04 | |
| 98 | +| RegexWithNewRegexString | a@b.c | 134.36 ns | 0.647 ns | 0.505 ns | 2.91 | 0.02 | |
| 99 | +| RegexWithNewRegexStringCompiled | a@b.c | 46.14 ns | 0.381 ns | 0.357 ns | 1.00 | 0.01 | |
| 100 | +| RegexWithStaticPartialMethod | a@b.c | 43.44 ns | 0.318 ns | 0.282 ns | 0.94 | 0.01 | |
| 101 | +| | | | | | | | |
| 102 | +| RegexWithStringPattern | anoth(...)mail@ [22] | 95.44 ns | 0.654 ns | 0.511 ns | 3.12 | 0.06 | |
| 103 | +| RegexWithNewRegexString | anoth(...)mail@ [22] | 95.70 ns | 0.518 ns | 0.459 ns | 3.13 | 0.06 | |
| 104 | +| RegexWithNewRegexStringCompiled | anoth(...)mail@ [22] | 30.60 ns | 0.623 ns | 0.583 ns | 1.00 | 0.03 | |
| 105 | +| RegexWithStaticPartialMethod | anoth(...)mail@ [22] | 30.02 ns | 0.152 ns | 0.142 ns | 0.98 | 0.02 | |
| 106 | +| | | | | | | | |
| 107 | +| RegexWithStringPattern | first(...)k.com [26] | 196.91 ns | 2.896 ns | 2.418 ns | 2.96 | 0.04 | |
| 108 | +| RegexWithNewRegexString | first(...)k.com [26] | 191.30 ns | 2.154 ns | 2.015 ns | 2.88 | 0.03 | |
| 109 | +| RegexWithNewRegexStringCompiled | first(...)k.com [26] | 66.47 ns | 0.463 ns | 0.411 ns | 1.00 | 0.01 | |
| 110 | +| RegexWithStaticPartialMethod | first(...)k.com [26] | 64.68 ns | 0.502 ns | 0.470 ns | 0.97 | 0.01 | |
| 111 | +| | | | | | | | |
| 112 | +| RegexWithStringPattern | invalid-email | 71.57 ns | 0.490 ns | 0.434 ns | 2.62 | 0.02 | |
| 113 | +| RegexWithNewRegexString | invalid-email | 69.20 ns | 0.600 ns | 0.501 ns | 2.53 | 0.02 | |
| 114 | +| RegexWithNewRegexStringCompiled | invalid-email | 27.32 ns | 0.182 ns | 0.161 ns | 1.00 | 0.01 | |
| 115 | +| RegexWithStaticPartialMethod | invalid-email | 26.40 ns | 0.080 ns | 0.071 ns | 0.97 | 0.01 | |
| 116 | +| | | | | | | | |
| 117 | +| RegexWithStringPattern | test@ | 65.96 ns | 0.679 ns | 0.567 ns | 2.60 | 0.02 | |
| 118 | +| RegexWithNewRegexString | test@ | 57.34 ns | 0.635 ns | 0.594 ns | 2.26 | 0.02 | |
| 119 | +| RegexWithNewRegexStringCompiled | test@ | 25.39 ns | 0.091 ns | 0.081 ns | 1.00 | 0.00 | |
| 120 | +| RegexWithStaticPartialMethod | test@ | 24.95 ns | 0.157 ns | 0.146 ns | 0.98 | 0.01 | |
| 121 | +| | | | | | | | |
| 122 | +| RegexWithStringPattern | user@domain | 119.30 ns | 0.742 ns | 0.658 ns | 2.27 | 0.02 | |
| 123 | +| RegexWithNewRegexString | user@domain | 115.68 ns | 1.357 ns | 1.269 ns | 2.20 | 0.03 | |
| 124 | +| RegexWithNewRegexStringCompiled | user@domain | 52.62 ns | 0.322 ns | 0.301 ns | 1.00 | 0.01 | |
| 125 | +| RegexWithStaticPartialMethod | user@domain | 45.81 ns | 0.315 ns | 0.263 ns | 0.87 | 0.01 | |
| 126 | +| | | | | | | | |
| 127 | +| RegexWithStringPattern | very.(...)e.com [60] | 312.25 ns | 2.419 ns | 2.145 ns | 3.44 | 0.03 | |
| 128 | +| RegexWithNewRegexString | very.(...)e.com [60] | 300.72 ns | 2.023 ns | 1.793 ns | 3.31 | 0.03 | |
| 129 | +| RegexWithNewRegexStringCompiled | very.(...)e.com [60] | 90.85 ns | 0.506 ns | 0.474 ns | 1.00 | 0.01 | |
| 130 | +| RegexWithStaticPartialMethod | very.(...)e.com [60] | 86.00 ns | 0.726 ns | 0.679 ns | 0.95 | 0.01 | |
| 131 | + |
| 132 | + |
| 133 | +**Conclusion:** Source-generated regex (`[GeneratedRegex]`) is consistently 3–13% faster than compiled regex and up to 3.4× faster than creating regex instances from string patterns, making it the clear winner for modern .NET applications. |
| 134 | + |
| 135 | + |
| 136 | +### Results for cold start (JIT impact) |
| 137 | +For these results, update the code to use only the two fastest options for better visibility: |
| 138 | +```csharp |
| 139 | +[SimpleJob(runStrategy:RunStrategy.ColdStart)] |
| 140 | +``` |
| 141 | +This uses `RunStrategy.ColdStart` to highlight JIT compilation impact. |
| 142 | + |
| 143 | +| Method | input | Mean | Error | StdDev | Median | Ratio | RatioSD | |
| 144 | +|-------------------------------- |--------------------- |---------:|----------:|----------:|---------:|------:|--------:| |
| 145 | +| RegexWithNewRegexStringCompiled | a@b.c | 57.66 us | 181.22 us | 534.34 us | 3.200 us | 18.12 | 183.58 | |
| 146 | +| RegexWithStaticPartialMethod | a@b.c | 25.86 us | 76.15 us | 224.52 us | 2.750 us | 8.12 | 77.15 | |
| 147 | +| | | | | | | | | |
| 148 | +| RegexWithNewRegexStringCompiled | anoth(...)mail@ [22] | 66.36 us | 211.66 us | 624.07 us | 2.600 us | 25.61 | 261.19 | |
| 149 | +| RegexWithStaticPartialMethod | anoth(...)mail@ [22] | 24.67 us | 68.12 us | 200.86 us | 4.150 us | 9.52 | 84.09 | |
| 150 | +| | | | | | | | | |
| 151 | +| RegexWithNewRegexStringCompiled | first(...)k.com [26] | 67.96 us | 218.74 us | 644.97 us | 2.800 us | 24.11 | 241.20 | |
| 152 | +| RegexWithStaticPartialMethod | first(...)k.com [26] | 37.99 us | 98.01 us | 288.99 us | 7.700 us | 13.48 | 108.11 | |
| 153 | +| | | | | | | | | |
| 154 | +| RegexWithNewRegexStringCompiled | invalid-email | 61.08 us | 197.36 us | 581.92 us | 2.400 us | 24.27 | 239.67 | |
| 155 | +| RegexWithStaticPartialMethod | invalid-email | 24.28 us | 71.96 us | 212.17 us | 2.550 us | 9.65 | 87.39 | |
| 156 | +| | | | | | | | | |
| 157 | +| RegexWithNewRegexStringCompiled | test@ | 57.73 us | 185.14 us | 545.88 us | 2.000 us | 27.12 | 274.84 | |
| 158 | +| RegexWithStaticPartialMethod | test@ | 23.39 us | 67.96 us | 200.37 us | 2.400 us | 10.99 | 100.90 | |
| 159 | +| | | | | | | | | |
| 160 | +| RegexWithNewRegexStringCompiled | user@domain | 64.72 us | 206.86 us | 609.94 us | 2.300 us | 25.15 | 257.44 | |
| 161 | +| RegexWithStaticPartialMethod | user@domain | 34.03 us | 102.84 us | 303.22 us | 2.900 us | 13.23 | 128.00 | |
| 162 | +| | | | | | | | | |
| 163 | +| RegexWithNewRegexStringCompiled | very.(...)e.com [60] | 63.45 us | 203.23 us | 599.24 us | 2.950 us | 20.72 | 203.79 | |
| 164 | +| RegexWithStaticPartialMethod | very.(...)e.com [60] | 45.92 us | 136.02 us | 401.05 us | 4.100 us | 15.00 | 136.40 | |
| 165 | + |
| 166 | + |
| 167 | +### Results with AOT compilation |
| 168 | +With AOT compilation enabled: |
| 169 | + |
| 170 | +| Method | input | Mean | Error | StdDev | Median | Ratio | RatioSD | |
| 171 | +|-------------------------------- |--------------------- |----------:|---------:|---------:|----------:|------:|--------:| |
| 172 | +| RegexWithNewRegexStringCompiled | a@b.c | 174.94 ns | 1.267 ns | 1.058 ns | 175.21 ns | 1.00 | 0.01 | |
| 173 | +| RegexWithStaticPartialMethod | a@b.c | 55.04 ns | 0.246 ns | 0.218 ns | 55.04 ns | 0.31 | 0.00 | |
| 174 | +| | | | | | | | | |
| 175 | +| RegexWithNewRegexStringCompiled | anoth(...)mail@ [22] | 110.63 ns | 1.908 ns | 1.784 ns | 109.74 ns | 1.00 | 0.02 | |
| 176 | +| RegexWithStaticPartialMethod | anoth(...)mail@ [22] | 41.26 ns | 0.668 ns | 0.592 ns | 41.15 ns | 0.37 | 0.01 | |
| 177 | +| | | | | | | | | |
| 178 | +| RegexWithNewRegexStringCompiled | first(...)k.com [26] | 241.25 ns | 3.879 ns | 3.439 ns | 240.85 ns | 1.00 | 0.02 | |
| 179 | +| RegexWithStaticPartialMethod | first(...)k.com [26] | 78.50 ns | 1.152 ns | 1.077 ns | 78.35 ns | 0.33 | 0.01 | |
| 180 | +| | | | | | | | | |
| 181 | +| RegexWithNewRegexStringCompiled | invalid-email | 87.19 ns | 1.743 ns | 3.188 ns | 85.76 ns | 1.00 | 0.05 | |
| 182 | +| RegexWithStaticPartialMethod | invalid-email | 40.07 ns | 0.740 ns | 0.692 ns | 39.96 ns | 0.46 | 0.02 | |
| 183 | +| | | | | | | | | |
| 184 | +| RegexWithNewRegexStringCompiled | test@ | 75.39 ns | 1.193 ns | 0.996 ns | 75.13 ns | 1.00 | 0.02 | |
| 185 | +| RegexWithStaticPartialMethod | test@ | 42.26 ns | 0.333 ns | 0.295 ns | 42.28 ns | 0.56 | 0.01 | |
| 186 | +| | | | | | | | | |
| 187 | +| RegexWithNewRegexStringCompiled | user@domain | 146.84 ns | 2.241 ns | 1.987 ns | 146.20 ns | 1.00 | 0.02 | |
| 188 | +| RegexWithStaticPartialMethod | user@domain | 60.37 ns | 0.656 ns | 0.548 ns | 60.36 ns | 0.41 | 0.01 | |
| 189 | +| | | | | | | | | |
| 190 | +| RegexWithNewRegexStringCompiled | very.(...)e.com [60] | 363.17 ns | 4.685 ns | 6.255 ns | 360.38 ns | 1.00 | 0.02 | |
| 191 | +| RegexWithStaticPartialMethod | very.(...)e.com [60] | 104.79 ns | 1.623 ns | 1.356 ns | 104.35 ns | 0.29 | 0.01 | |
| 192 | + |
| 193 | +Source-generated regex with `[GeneratedRegex]` consistently outperforms `RegexOptions.Compiled` by 2–3.5× in AOT-compiled scenarios, making it the clear choice for modern .NET applications where both performance and native compilation matter. |
0 commit comments