.NET 7的性能改进有哪些具体细节?
摘要:原文 | Stephen Toub 翻译 | 郑子铭 矢量化 (Vectorization) SIMD,即单指令多数据 (Single Instruction Multiple Data),是一种处理方式,其中一条指令同时适用于多条数据。你
原文 | Stephen Toub
翻译 | 郑子铭
矢量化 (Vectorization)
SIMD,即单指令多数据 (Single Instruction Multiple Data),是一种处理方式,其中一条指令同时适用于多条数据。你有一个数字列表,你想找到一个特定值的索引?你可以在列表中一次比较一个元素,这在功能上是没有问题的。但是,如果在读取和比较一个元素的相同时间内,你可以读取和比较两个元素,或四个元素,或32个元素呢?这就是SIMD,利用SIMD指令的艺术被亲切地称为 "矢量化",其中操作同时应用于一个 "矢量 "中的所有元素。
.NET长期以来一直以Vector的形式支持矢量化,它是一种易于使用的类型,具有一流的JIT支持,使开发者能够编写矢量化的实现。Vector最大的优点之一也是它最大的缺点之一。该类型被设计为适应你的硬件中可用的任何宽度的向量指令。如果机器支持256位宽度的向量,很好,这就是Vector的目标。如果不支持,如果机器支持128位宽度的向量,很好,这就是Vector的目标。但是这种灵活性有各种缺点,至少在今天是这样;例如,你可以在Vector上执行的操作最终需要与所用向量的宽度无关,因为宽度是根据代码实际运行的硬件而变化的。这意味着可以在Vector上进行的操作是有限的,这反过来又限制了可以用它来矢量化的操作种类。另外,由于它在一个给定的进程中只有单一的大小,一些介于128位和256位之间的数据集大小可能不会像你希望的那样被处理好。你写了基于Vector的算法,并在一台支持256位向量的机器上运行,这意味着它一次可以处理32个字节,但是你给它的输入是31个字节。如果Vector映射到128位向量,它就可以用来改善对该输入的处理,但是由于它的向量大小大于输入数据的大小,实现最终会退回到没有加速的状态。还有与R2R和Native AOT有关的问题,因为超前编译需要事先知道哪些指令应该用于Vector操作。你在前面讨论DOTNET_JitDisasmSummary的输出时已经看到了这一点;我们看到NarrowUtf16ToAscii方法是 "hello, world "控制台应用程序中仅有的几个被JIT编译的方法之一,这是因为它由于使用Vector而缺乏R2R代码。
从.NET Core 3.0开始,.NET获得了数以千计的新的 "硬件本征 "方法,其中大部分是映射到这些SIMD指令之一的.NET API。这些内在因素使专家能够编写一个针对特定指令集的实现,如果做得好,可以获得最好的性能,但这也要求开发者了解每个指令集,并为每个可能相关的指令集实现他们的算法,例如,如果支持AVX2实现,或支持SSE2实现,或支持ArmBase实现,等等。
.NET 7引入了一个中间地带。以前的版本引入了Vector128和Vector256类型,但纯粹是作为数据进出硬件本征的载体,因为它们都与特定宽度的向量有关。现在在.NET 7中,通过dotnet/runtime#53450、dotnet/runtime#63414、dotnet/runtime#60094和dotnet/runtime#68559,在这些类型上也定义了大量的跨平台操作,例如Vector128.ExtractMostSignificantBits、Vector256.ConditionalSelect,等等。想要或需要超越高层Vector提供的内容的开发者可以选择针对这两种类型中的一种或多种。通常情况下,开发者会在Vector128的基础上编写一条代码路径,因为这条路径的覆盖面最广,可以从矢量化中获得大量的收益,然后如果有动力的话,可以为Vector256添加第二条路径,以便在拥有256位宽度矢量的平台上进一步增加吞吐量。把这些类型和方法看作是一个平台抽象层:你向这些方法编码,然后JIT将它们翻译成最适合底层平台的指令。考虑一下这个简单的代码作为一个例子。
