.NET 10 的正则性能现在什么水平?我拿它和 Go、Python、C++、PCRE2 测了一轮

前两天看到 apocelipes 写的这篇文章:

https://www.cnblogs.com/apocelipes/p/21055314

文章测了 C++ 标准库、PCRE2、Go、Python,还有一个 Go 的代码生成方案。结论挺有意思,不过我看完第一反应是:怎么没有 .NET?

作为一个长期写 .NET 的人,看到这种正则性能对比没有 .NET,总觉得桌上少了个熟人。尤其是这几年 .NET 的 Regex 引擎其实变化很大,GeneratedRegexNonBacktracking、各种查找优化都已经不是当年那套印象了。所以我就顺手搭了一个仓库,想看看 .NET 10 放进去之后大概会在什么位置。

代码在这里:

https://github.com/sdcb/regex-perf-test

先说结论:在我这份公开合成的数据集上,.NET 10 的 GeneratedRegex 排第一

当然,这句话有前提,后面会说。

为什么不用原文数据?

原文最大的问题不是代码写得怎样,而是测试数据没有给。

正则性能很吃输入分布。比如:

  • 每行有多长
  • 命中比例是多少
  • TSLA 出现在行首还是行尾
  • 不命中的行是不是有很多"差一点命中"的内容
  • 引号和转义字符有多少
  • .*? 中间到底要跨多少字符

这些东西一变,结果就可能变。

所以我没有试图"复现原文结果"。原文数据不公开,就没法严肃复现。我这里做的是另一件事:用公开、确定性的规则合成一份数据,然后把所有代码放出来,让别人可以重新跑。

数据生成脚本在仓库里,规则很简单:

  • 生成约 1GB 的文本文件
  • 每行是一个 JSON string literal
  • JSON string 里面再放一个紧凑 JSON object
  • 每 20 行放一条 TSLA,也就是大约 5% 命中
  • 固定随机种子,确保别人生成出来的数据一致

实际这次生成的数据是:

text 复制代码
bytes: 1,073,741,855
lines: 4,624,532
expected matches: 231,227
pattern: \"TSLA.*?\"
seed: 20260702

所有实现都先把文件读进内存,然后只统计内存中扫描匹配的时间。读取时间也记录了,但不参与排序。

测了哪些实现?

这次一共测了 10 个:

case 说明
MSVC std::regex Visual Studio 18 / MSVC STL
MinGW std::regex GCC 16.1.0 / libstdc++
PCRE2 vcpkg PCRE2 10.47
PCRE2 JIT 显式 pcre2_jit_match
Python re Python 3.12.4
Go regexp Go 1.26.2
.NET Regex 普通 Regex,构造一次复用
.NET Compiled RegexOptions.Compiled,构造一次复用
.NET GeneratedRegex source generator 生成的正则
.NET NonBacktracking RegexOptions.NonBacktracking

这里有个小点要说明:普通 new Regex(...) 不是每行都 new 一次,而是构造一次后复用。每行都 new 那属于测错误用法,不是正常业务热路径。

.NET 这边大概是这样:

csharp 复制代码
private const string Pattern = "\\\"TSLA.*?\\\"";

private static readonly Regex PlainRegex =
    new(Pattern, RegexOptions.CultureInvariant);

private static readonly Regex CompiledRegex =
    new(Pattern, RegexOptions.CultureInvariant | RegexOptions.Compiled);

private static readonly Regex NonBacktrackingRegex =
    new(Pattern, RegexOptions.CultureInvariant | RegexOptions.NonBacktracking);

[GeneratedRegex(Pattern, RegexOptions.CultureInvariant)]
private static partial Regex InfoLineRegex();

核心循环也没什么花活:

csharp 复制代码
private static long MatchLines(string[] lines, Regex regex)
{
    long matches = 0;
    foreach (var line in lines)
    {
        if (regex.IsMatch(line))
        {
            matches++;
        }
    }
    return matches;
}

C++ 的 PCRE2 JIT 也不是"编译了 JIT 然后还调用普通 match"这种模糊写法,而是明确调用 pcre2_jit_match。完整代码直接看 GitHub 就行,这里不贴一大坨了。

结果

每个 case:

  • warmup 1 次
  • 正式跑 3 次
  • 只统计扫描匹配时间
  • 所有 case 匹配数都必须等于 231,227

结果如下,按平均耗时排序:

排名 实现 平均单轮扫描
1 .NET GeneratedRegex 125.566 ms
2 .NET Regex 170.996 ms
3 .NET RegexOptions.Compiled 171.583 ms
4 .NET RegexOptions.NonBacktracking 219.780 ms
5 Go regexp 303.017 ms
6 MSVC std::regex 448.698 ms
7 PCRE2 JIT 612.498 ms
8 Python re 894.424 ms
9 PCRE2 5,191.270 ms
10 MinGW/libstdc++ std::regex 26,272.900 ms

.NET GeneratedRegex 三轮分别是:

text 复制代码
125.294 ms
125.734 ms
125.669 ms

这个波动很小,所以至少在这份数据上,不像是偶然抖出来的。

另外我一开始也跑过 1MB 的小数据,那个时候 .NET 没这么明显的优势。这个也正常,1MB 太小了,JIT、tiered compilation、缓存状态、计时噪声都能影响结果。到了 1GB 之后,差距就稳定多了。

有几个结果挺有意思

第一,.NET GeneratedRegex 真的很猛。

这个结果对我来说有点爽,但不是完全意外。现在的 .NET Regex 对这类模式优化得确实很激进,source generator 又能把一些工作提前到编译期。这个 pattern 本身也简单,本质上更接近"找一个固定前缀,然后扫到后面的引号"。

第二,RegexOptions.Compiled 这次没赢普通 Regex

两者基本持平,普通 Regex 还略快一点点:

text 复制代码
.NET Regex:              170.996 ms
.NET RegexOptions.Compiled: 171.583 ms

所以现在写 .NET,别再机械地觉得 Compiled 一定更快。很多时候你真正应该先考虑的是 [GeneratedRegex]Compiled 不是不能用,而是别把它当成无脑性能开关。

第三,NonBacktracking 也不等于更快。

这次它是 219.780 ms,比普通 .NET Regex 慢一些。NonBacktracking 的重点是避免灾难性回溯、提供更稳的线性行为,不是承诺所有正则都更快。

第四,PCRE2 JIT 这次没有赢。

这和很多人的直觉可能不一样。PCRE2 JIT 很强,但不是每个 pattern、每份数据都会碾压。这个 case 里 .NET 的路径显然更适合。

这里我也特意检查了一下,免得变成"JIT 其实没开"的低级问题:PCRE2 用的是 8-bit API,pcre2_compilepcre2_jit_compilepcre2_match_data_create_from_pattern 都在计时循环外;JIT case 检查了 pcre2_jit_compile 的返回值,失败会直接报错;真正扫描时调用的是 pcre2_jit_match,不是普通的 pcre2_match。所有实现也都是逐行 IsMatch/search,不是某些实现扫全文件、某些实现逐行扫。

所以 PCRE2 JIT 输给 MSVC std::regex 这件事我也觉得值得多看一眼,但目前看不是因为测试代码把 JIT 写错了。更可能是这个 pattern 和这份数据刚好不在 PCRE2 JIT 最舒服的区间。

第五,MinGW/libstdc++ 的 std::regex 还是那个味。

1GB 单轮 26 秒多。这个结果非常突出,突出到我都检查了好几遍匹配数。最后所有实现匹配数一致,所以至少这个 case 下它确实很慢。

怎么复现?

仓库:

https://github.com/sdcb/regex-perf-test

快速冒烟:

powershell 复制代码
.\run.ps1 -SizeMB 10 -Iterations 1 -Warmup 0 -Regenerate

正式跑默认 1GB:

powershell 复制代码
.\run.ps1 -Regenerate

输出在:

text 复制代码
results/results.csv
results/report.md
results/raw.jsonl

我本机用到的环境大概是:

text 复制代码
.NET runtime: 10.0.9
Go: 1.26.2
Python: 3.12.4
MSVC: 19.51
GCC: 16.1.0
PCRE2: 10.47

最后稳妥说一句

这篇文章不是想证明".NET 正则天下第一"。

更准确的说法是:

在这份公开合成的数据集、这个 pattern \"TSLA.*?\"、这个命中比例和行长度分布下,.NET 10 的 GeneratedRegex 是本轮最快的实现。

换一个正则,换一份数据,尤其是换成复杂捕获、lookaround、大量失败回溯、不同编码和不同命中分布,排名都可能变。

但至少这轮结果能说明一件事:现在做正则性能对比,如果不把 .NET 放进去,确实容易漏掉一个很能打的选手。

作为 .NET 用户,看到这个结果还是挺开心的。不是因为它永远第一,而是因为这些年 .NET runtime 的优化,是真的落到了这种普通代码能直接吃到的地方。

欢迎大家加入我的微信群:Chats交流群

.NET骚操作QQ群:495782587