引言:关于正则表达式速度的误区
\n\n如果你曾经用 C# 编写过代码并使用过正则表达式,你很可能遇到过这样的两难选择:使用 \\d 还是 [0-9] 来查找数字?乍一看没有区别——两者都能找到数字。但那些追求极致性能的经验丰富的开发者知道:\\d 可能比 [0-9] 慢得多。
在 Stack Overflow 上,这个问题获得了数千次浏览和数十条评论。今天,我们不仅仅是复述答案,而是深入 .NET Regex 引擎的底层,弄清楚为什么会这样以及它如何影响你的代码。我们将用 C# 编写真实的基准测试,查看 IL 代码,并了解何时应该警惕,何时可以放松。
\n\n准备好了吗?开始吧!
\n\nC# 中的 \\d 和 [0-9] 是什么?
\n\n首先——基础知识。在大多数正则表达式实现中(包括 C#),\\d 是“任何数字”的快捷方式。而 [0-9] 是一个字符类,显式列举了从 '0' 到 '9' 的范围。
看起来是一样的。但魔鬼,总是在细节中。
\n\nUnicode vs ASCII
\n\n在 .NET 中(从 .NET Framework 4.5 开始),\\d 默认匹配Unicode 类别 Nd(数字,十进制数字)中的任何字符。这不仅包括阿拉伯数字 0-9,还包括:
- \n
- 天城文数字(०१२३...) \n
- 泰米尔文数字 \n
- 东阿拉伯数字 \n
- 以及其他数十种数字系统 \n
而 [0-9] 是硬编码的 ASCII 码范围(48-57)。只有 0,1,2,3,4,5,6,7,8,9。没有 Unicode。
为什么速度不同?底层解析
\n\n现在关键问题:为什么 \\d 更慢?让我们从引擎层面来理解。
1. 检查次数
\n\n当引擎遇到 [0-9] 时,它做一件事:检查字符代码是否在 48-57 范围内。这是一次比较(如果考虑边界,则是两次)。
当引擎遇到 \\d 时,它需要:
- \n
- 获取字符的 Unicode 类别(这已经比简单的 int 操作更昂贵)。 \n
- 将其与
Nd类别进行比较。 \n
Unicode 类别不仅仅是一个数字,而是一个通过内部表计算的完整标志。对于拉丁数字,结果相同,但到达结果的路径更长。
\n\n2. 缓存与 JIT
\n\n在 .NET 中,Regex 被编译为中间代码(IL)甚至本机代码(如果使用 RegexOptions.Compiled)。对于 [0-9],JIT 可以生成非常简单的指令序列:几次比较和条件跳转。对于 \\d,则需要调用 CharUnicodeInfo.GetUnicodeCategory 方法并进行后续比较。
方法调用有开销:参数压栈、调用、栈检查。即使方法很小,它仍然比内联比较慢。
\n\n3. 真实基准测试示例
\n\n让我们用 C# 编写一个简单的测试,使用 BenchmarkDotNet(如果你要重复测试,请务必通过 NuGet 安装它):
\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\n结果(在 .NET