另一个改进是在处理RegexOptions.IgnoreCase方面。IgnoreCase的实现使用char.ToLower{Invariant}以获得要比较的相关字符,但由于区域性特定的映射,这样做会带来一些开销。dotnet/runtime#35185允许在唯一可能与被比较字符小写的字符是该字符本身时避免这些开销。
IsMatch .NET FW 4.8 2,558.1 ns 1.00
IsMatch .NET Core 3.1 789.3 ns 0.31
IsMatch .NET 5.0 129.0 ns 0.05
与此相关的改进是dotnet/runtime#35203,它也服务于RegexOptions。IgnoreCase减少了实现对CultureInfo进行的虚拟调用的数量。缓存TextInfo,而不是CultureInfo从它来。
IsMatch .NET FW 4.8 712.9 ns 1.00
IsMatch .NET Core 3.1 343.5 ns 0.48
IsMatch .NET 5.0 100.9 ns 0.14
最近我最喜欢的优化之一是dotnet/runtime#35824(随后在dotnet/runtime#35936中进一步增强)。regex的承认变化,从一个原子环(一个明确的书面或更常见的一个原子的升级到自动的分析表达式),我们可以更新扫描循环中的下一个起始位置(再一次,详见博客)基于循环的结束,而不是开始。对于许多输入,这可以大大减少开销。使用基准测试和来自https://github.com/mariomka/regex benchmark的数据:
Email .NET FW 4.8 1,036.729 ms 1.00
Email .NET Core 3.1 930.238 ms 0.90
Email .NET 5.0 50.911 ms 0.05
Uri .NET FW 4.8 870.114 ms 1.00
Uri .NET Core 3.1 759.079 ms 0.87
Uri .NET 5.0 50.022 ms 0.06
IP .NET FW 4.8 75.718 ms 1.00
IP .NET Core 3.1 61.818 ms 0.82
IP .NET 5.0 6.837 ms 0.09
最后,并不是所有的焦点都集中在实际执行正则表达式的原始吞吐量上。开发人员使用Regex获得最佳吞吐量的方法之一是指定RegexOptions。编译,它使用反射发射在运行时生成IL,反过来需要JIT编译。根据所使用的表达式,Regex可能会输出大量IL,然后需要大量的JIT处理才能生成汇编代码。dotnet/runtime#35352改进了JIT本身来帮助解决这种情况,修复了regex生成的IL触发的一些可能的二次执行时代码路径。而dotnet/runtime#35321对Regex引擎使用的IL操作进行了调整,使其使用的模式更接近于c#编译器发出的模式,这一点很重要,因为JIT对这些模式进行了更多的优化。在一些具有数百个复杂正则表达式的实际工作负载上,将它们组合起来可以将JIT表达式所花的时间减少20%以上。