【翻译】.NET 5中的性能改进 (8)


与此相关的是类型检查。我之前提到过Span解决了很多问题,但也引入了新的模式,从而推动了系统其他领域的改进;对于Span本身的实现也是这样。 Span 构造函数做协方差检查,要求T[]实际上是T[]而不是U[],其中U源自T,例如:

using System; class Program { static void Main() => new Span<A>(new B[42]); } class A { } class B : A { }

将导致异常:

System.ArrayTypeMismatchException: Attempted to access an element as a type incompatible with the array


该异常源于对Span 的构造函数的

if (!typeof(T).IsValueType && array.GetType() != typeof(T[])) ThrowHelper.ThrowArrayTypeMismatchException();

PR dotnet/runtime#32790就是这样优化数组的.GetType()!= typeof(T [])检查何时密封T,而dotnet/runtime#1157识别typeof(T).IsValueType模式并将其替换为常量 值(PR dotnet/runtime#1195对于typeof(T1).IsAssignableFrom(typeof(T2))进行了相同的操作)。 这样做的最终结果是极大地改善了微基准,例如:

class A { } sealed class B : A { } private B[] _array = new B[42]; [Benchmark] public int Ctor() => new Span<B>(_array).Length;

我得到的结果如下:

Method Runtime Mean Ratio Code Size
Ctor   .NET FW 4.8   48.8670 ns   1.00   66 B  
Ctor   .NET Core 3.1   7.6695 ns   0.16   66 B  
Ctor   .NET 5.0   0.4959 ns   0.01   17 B  


当查看生成的程序集时,差异的解释就很明显了,即使不是完全精通程序集代码。以下是[DisassemblyDiagnoser]在.NET Core 3.1上生成的内容:

; Program.Ctor() push rdi push rsi sub rsp,28 mov rsi,[rcx+8] test rsi,rsi jne short M00_L00 xor eax,eax jmp short M00_L01 M00_L00: mov rcx,rsi call System.Object.GetType() mov rdi,rax mov rcx,7FFE4B2D18AA call CORINFO_HELP_TYPEHANDLE_TO_RUNTIMETYPE cmp rdi,rax jne short M00_L02 mov eax,[rsi+8] M00_L01: add rsp,28 pop rsi pop rdi ret M00_L02: call System.ThrowHelper.ThrowArrayTypeMismatchException() int 3 ; Total bytes of code 66

下面是.NET5的内容:

; Program.Ctor() mov rax,[rcx+8] test rax,rax jne short M00_L00 xor eax,eax jmp short M00_L01 M00_L00: mov eax,[rax+8] M00_L01: ret ; Total bytes of code 17

另一个例子是,在前面的GC讨论中,我提到了将本地运行时代码移植到c#代码中所带来的一些好处。有一点我之前没有提到,但现在将会提到,那就是它导致了我们对系统进行了其他改进,解决了移植的关键阻滞剂,但也改善了许多其他情况。一个很好的例子是dotnet/runtime#38229。当我们第一次将本机数组排序实现移动到managed时,我们无意中导致了浮点值的回归,这个回归被@nietras 发现,随后在dotnet/runtime#37941中修复。回归是由于本机实现使用一个特殊的优化,我们失踪的管理端口(浮点数组,将所有NaN值数组的开始,后续的比较操作可以忽略NaN)的可能性,我们成功了。然而,问题是这个的方式表达并没有导致大量的代码重复:本机实现模板,使用和管理实现使用泛型,但限制与泛型等,内联 helpers介绍,以避免大量的代码重复导致non-inlineable在每个比较采用那种方法调用。PR dotnet/runtime#38229通过允许JIT在同一类型内嵌共享泛型代码解决了这个问题。考虑一下这个微基准测试:

private C c1 = new C() { Value = 1 }, c2 = new C() { Value = 2 }, c3 = new C() { Value = 3 }; [Benchmark] public int Compare() => Comparer<C>.Smallest(c1, c2, c3); class Comparer<T> where T : IComparable<T> { public static int Smallest(T t1, T t2, T t3) => Compare(t1, t2) <= 0 ? (Compare(t1, t3) <= 0 ? 0 : 2) : (Compare(t2, t3) <= 0 ? 1 : 2); [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int Compare(T t1, T t2) => t1.CompareTo(t2); } class C : IComparable<C> { public int Value; public int CompareTo(C other) => other is null ? 1 : Value.CompareTo(other.Value); }

最小的方法比较提供的三个值并返回最小值的索引。它是泛型类型上的一个方法,它调用同一类型上的另一个方法,这个方法反过来调用泛型类型参数实例上的方法。由于基准使用C作为泛型类型,而且C是引用类型,所以JIT不会专门为C专门化此方法的代码,而是使用它生成的用于所有引用类型的“shared”实现。为了让Compare方法随后调用到CompareTo的正确接口实现,共享泛型实现使用了一个从泛型类型映射到正确目标的字典。在. net的早期版本中,包含那些通用字典查找的方法是不可行的,这意味着这个最小的方法不能内联它所做的三个比较调用,即使Compare被归为methodimploptions .侵略化的内联。前面提到的PR消除了这个限制,在这个例子中产生了一个非常可测量的加速(并使数组排序回归修复可行):

Method Runtime Mean Ratio
Compare   .NET FW 4.8   8.632 ns   1.00  
Compare   .NET Core 3.1   9.259 ns   1.07  
Compare   .NET 5.0   5.282 ns   0.61  

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wsssyf.html