.NET 7的性能改进有哪些具体细节?
摘要:原文 | Stephen Toub 翻译 | 郑子铭 New APIs 在.NET 7中,Regex得到了几个新的方法,所有这些方法都能提高性能。新的API的简单性可能也误导了为实现它们所需的工作量,特别是由于新的API都支持ReadOnl
原文 | Stephen Toub
翻译 | 郑子铭
New APIs
在.NET 7中,Regex得到了几个新的方法,所有这些方法都能提高性能。新的API的简单性可能也误导了为实现它们所需的工作量,特别是由于新的API都支持ReadOnlySpan输入到regex引擎。
dotnet/runtime#65473将Regex带入了基于跨度的.NET时代,克服了Regex自跨度在.NET Core 2.1中引入后的一个重要限制。Regex在历史上一直是基于处理System.String输入的,这一事实贯穿了Regex的设计和实现,包括在.NET Framework中依赖的扩展性模型Regex.CompileToAssembly所暴露的API(CompileToAssembly现在已经被淘汰,在.NET Core中从未发挥作用)。依赖于字符串作为输入的性质的一个微妙之处在于如何将匹配信息返回给调用者。Regex.Match返回一个Match对象,代表输入中的第一个匹配,而这个Match对象暴露了一个NextMatch方法,可以移动到下一个匹配。这意味着Match对象需要存储对输入的引用,这样它就可以作为NextMatch调用的一部分被反馈到匹配引擎。如果这个输入是一个字符串,很好,没有问题。但是如果输入的是一个ReadOnlySpan,这个跨度作为一个引用结构就不能存储在Match类对象上,因为引用结构只能在堆栈而不是堆上。仅仅这一点就使支持跨度成为一个挑战,但问题甚至更加根深蒂固。所有的 regex 引擎都依赖于 RegexRunner,它是一个基类,上面存储了所有必要的状态,以反馈给构成正则表达式实际匹配逻辑的 FindFirstChar 和 Go 方法(这些方法包含执行匹配的所有核心代码,其中 FindFirstChar 是一种优化,用于跳过不可能开始匹配的输入位置,然后 Go 执行实际匹配逻辑)。如果你看一下内部的RegexInterpreter类型,也就是当你构造一个新的Regex(...)而不使用RegexOptions.Compiled或RegexOptions.NonBacktracking标志时得到的引擎,它来源于RegexRunner。同样,当你使用RegexOptions.Compiled时,它把它反射的动态方法交给了一个从RegexRunner派生的类型,RegexOptions.NonBacktracking有一个SymbolicRegexRunnerFactory,产生从RegexRunner派生的类型,以此类推。这里最相关的是,RegexRunner是公共的,因为由Regex.CompileToAssembly类型(以及现在的regex源代码生成器)生成的类型包括从这个RegexRunner派生的类型。因此,那些FindFirstChar和Go方法是抽象的、受保护的、无参数的,因为它们从基类上受保护的成员中获取它们需要的所有状态。这包括要处理的字符串输入。那么,跨度呢?我们当然可以对一个输入的ReadOnlySpan调用ToString()。这在功能上是正确的,但却完全违背了接受跨度的目的,更糟糕的是,这可能会导致消费应用程序的性能比没有API时更差。相反,我们需要一种新的方法和新的API。
首先,我们使FindFirstChar和Go成为虚拟的,而不是抽象的。分割这些方法的设计在很大程度上是过时的,特别是强制分离了一个处理阶段,即找到匹配的下一个可能的位置,然后是在该位置实际执行匹配的阶段,这与所有的引擎并不一致,比如NonBacktracking使用的引擎(它最初将FindFirstChar作为一个nop实现,并将其所有逻辑放在Go中)。然后我们添加了一个新的虚拟扫描方法,重要的是,它需要一个ReadOnlySpan作为参数;这个span不能从基本的RegexRunner中暴露出来,必须被传递进去。然后,我们在Scan方面实现了FindFirstChar和Go,并使它们 "只是工作"。然后,所有的引擎都是以这个跨度来实现的;它们不再需要访问受保护的RegexRunner.runtext、RegexRunner.runtextbeg和RegexRunner.runtextend成员,它们只是被交给跨度,已经切成了输入区域,并进行处理。从性能的角度来看,这样做的一个好处是使JIT能够更好地消除各种开销,特别是围绕边界检查。当逻辑以字符串的形式实现时,除了输入字符串本身之外,引擎还被告知要处理的输入区域的开头和结尾(因为开发者可以调用类似Regex.Match(string input, int beginning, int length)的方法,以便只处理一个子串)。
