.NET 7的性能改进有哪些具体细节?

摘要:原文 | Stephen Toub 翻译 | 郑子铭 代码生成 (Code generation) .NET 7的regex实现有不少于四个引擎:解释器(如果你不明确选择其他引擎,你会得到什么),编译器(你用RegexOptions.Com
原文 | Stephen Toub 翻译 | 郑子铭 代码生成 (Code generation) .NET 7的regex实现有不少于四个引擎:解释器(如果你不明确选择其他引擎,你会得到什么),编译器(你用RegexOptions.Compiled得到什么),非回溯引擎(你用RegexOptions.NonBacktracking得到什么),以及源生成器(你用[GeneratedRegex(..)]得到什么)。解释器和非反向追踪引擎不需要任何类型的代码生成;它们都是基于创建内存中的数据结构,表示如何根据模式匹配输入。不过,其他两个都会生成特定于模式的代码;生成的代码试图模仿你可能写的代码,如果你根本不使用Regex,而是直接写代码来执行类似的匹配。源码生成器吐出的是直接编译到你的汇编中的C#,而编译器在运行时通过反射emit吐出IL。这些都是针对模式生成的代码,这意味着有大量的机会可以优化。 dotnet/runtime#59186提供了源代码生成器的初始实现。这是编译器的直接移植,有效地将IL逐行翻译成C#;结果是C#,类似于你通过ILSpy等反编译器运行生成的IL。一系列的PR接着对源码生成器进行了迭代和调整,但最大的改进来自于对编译器和源码生成器的共同改变。在.NET 5之前,编译器吐出的IL与解释器的工作非常相似。解释器收到了一系列指令,它逐一进行解释,而编译器收到了同样的一系列指令,只是发出了处理每个指令的IL。它有一些提高效率的机会,如循环解卷,但很多价值被留在了桌子上。在.NET 5中,为了支持没有回溯的模式,增加了另一种路径;这种代码路径是基于被解析的节点树,而不是基于一系列的指令,这种更高层次的形式使编译器能够获得更多关于模式的见解,然后可以用来生成更有效的代码。在.NET 7中,对所有regex特性的支持都是在多个PR的过程中逐步加入的,特别是dotnet/runtime#60385用于回溯单字符循环,dotnet/runtime#61698用于回溯单字符懒惰循环,dotnet/runtime#61784用于其他回溯懒惰循环,dotnet/runtime#61906用于其他回溯循环以及回引和条件。在这一点上,唯一缺少的功能是对RegexOptions.RightToLeft和lookbehinds的支持(这是以从右到左的方式实现的),而且我们根据这些功能相对较少的使用情况决定,我们没有必要为了启用它们而保留旧的编译器代码。所以,dotnet/runtime#62318删除了旧的实现。但是,尽管这些功能相对较少,但说一个 "支持所有模式 "的故事比说一个需要特殊调用和异常的故事要容易得多,所以dotnet/runtime#66127和dotnet/runtime#66280添加了完整的lookbehind和RightToLeft支持,这样就不会有回溯了。在这一点上,编译器和源代码生成器现在都支持编译器以前所做的一切,但现在有了更现代化的代码生成。这种代码生成反过来又使之前讨论的许多优化成为可能,例如,它提供了使用LastIndexOf等API作为回溯的一部分的机会,这在以前的方法中几乎是不可能的。 源码生成器发出成语C#的好处之一是它使迭代变得容易。每次你输入一个模式并看到生成器发出的东西,就像被要求对别人的代码进行审查一样,你经常看到一些值得评论的 "新 "东西,或者在这种情况下,改进生成器以解决这个问题。因此,一堆PR的起源是基于审查生成器发出的东西,然后调整生成器以做得更好(由于编译器实际上是和源生成器一起完全重写的,它们保持相同的结构,很容易从一个移植到另一个的改进)。例如,dotnet/runtime#68846和dotnet/runtime#69198调整了一些比较的执行方式,以便向JIT传达足够的信息,从而消除一些后续的边界检查,dotnet/runtime#68490识别了在一些可静态观察的情况下不可能发生的各种条件,并能够消除所有这些代码基因。同样明显的是,有些模式不需要扫描循环的全部表现力,可以使用更紧凑和定制的扫描实现。dotnet/runtime#68560做到了这一点,例如,像hello这样的简单模式根本不会发出一个循环,而会有一个更简单的扫描实现,比如。
阅读全文