Introduction: The myth of regular expression speed
\n\nIf you've ever written in C# and used regular expressions, you've likely faced the dilemma: use \\d or [0-9] to find digits? At first glance, there's no difference — both find digits. But experienced developers who polish performance to a shine know: \\d can be significantly slower than [0-9].
On Stack Overflow, this question has gathered thousands of views and dozens of comments. Today, we won't just retell the answer, but will look under the hood of the .NET Regex engine and understand why this happens and how it affects your code. We'll write real benchmarks in C#, look at IL code, and find out when to sound the alarm and when you can relax.
\n\nReady? Let's go!
\n\nWhat are \\d and [0-9] in C#?
\n\nFirst — the basics. In most regular expression implementations (including C#), \\d is a shortcut for "any digit". And [0-9] is a character class that explicitly lists the range from '0' to '9'.
It seems like the same thing. But the devil, as always, is in the details.
\n\nUnicode vs ASCII
\n\nIn .NET (starting from .NET Framework 4.5 and later), \\d by default matches any character from the Unicode category Nd (Number, Decimal Digit). This is not only Arabic digits 0-9, but also:
- \n
- Devanagari digits (०१२३...) \n
- Tamil script digits \n
- Eastern Arabic digits \n
- And dozens of other numeral systems \n
And [0-9] is a hardcoded range of ASCII codes (48-57). Only 0,1,2,3,4,5,6,7,8,9. No Unicode.
Why is the speed different? Under the hood analysis
\n\nNow the main point: why is \\d slower? Let's break it down at the engine level.
1. Number of checks
\n\nWhen the engine encounters [0-9], it does one thing: checks if the character code is in the range 48-57. This is one comparison (or two, if considering boundaries).
When the engine encounters \\d, it must:
- \n
- Get the Unicode category of the character (this is already more expensive than just an int). \n
- Compare it with the
Ndcategory. \n
The Unicode category is not just a number, it's a whole flag that is computed through internal tables. For Latin digits, the result will be the same, but the path to it is longer.
\n\n2. Caching and JIT
\n\nIn .NET, Regex is compiled into intermediate code (IL) or even native code (if RegexOptions.Compiled is used). For [0-9], the JIT can generate a very simple sequence of instructions: a couple of comparisons and a conditional jump. For \\d — a call to the CharUnicodeInfo.GetUnicodeCategory method and a subsequent comparison.
A method call has overhead: pushing arguments, call, stack check. Even if the method is small, it's still slower than an inline comparison.
\n\n3. Example from a real benchmark
\n\nLet's write a simple test in C# using BenchmarkDotNet (be sure to install it via NuGet if you want to repeat this):
\n\nusing BenchmarkDotNet.Attributes;\nusing BenchmarkDotNet.Running;\nusing System.Text.RegularExpressions;\n\npublic class RegexBenchmark\n{\n private static readonly Regex _digitPattern = new Regex(@"\\d", RegexOptions.Compiled);\n private static readonly Regex _rangePattern = new Regex(@"[0-9]", RegexOptions.Compiled);\n \n private const string TestString = "Hello123World456!";\n\n [Benchmark]\n public bool DigitPattern() => _digitPattern.IsMatch(TestString);\n\n [Benchmark]\n public bool RangePattern() => _rangePattern.IsMatch(TestString);\n}\n\nclass Program\n{\n static void Main() => BenchmarkRunner.Run<RegexBenchmark>();\n}\n\nResults (on .NET