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


System.Collections。不可变的版本也有改进。dotnet/runtime#1183是@hnrqbaggio通过添加[MethodImpl(methodimploptions.ancsiveinlining)]到ImmutableArray的GetEnumerator方法来提高对ImmutableArray的GetEnumerator方法的foreach性能。我们通常非常谨慎洒AggressiveInlining:它可以使微基准测试看起来很好,因为它最终消除调用相关方法的开销,但它也可以大大提高代码的大小,然后一大堆事情产生负面影响,如导致指令缓存变得不那么有效了。然而,在这种情况下,它不仅提高了吞吐量,而且实际上还减少了代码的大小。内联是一种强大的优化,不仅因为它消除了调用的开销,还因为它向调用者公开了被调用者的内容。JIT通常不做过程间分析,这是由于JIT用于优化的时间预算有限,但是内联通过合并调用者和被调用者克服了这一点,在这一点上调用者因素的JIT优化被调用者因素。假设一个方法public static int GetValue() => 42;调用者执行if (GetValue() * 2 > 100){…很多代码…}。如果GetValue()没有内联,那么比较和“大量代码”将会被JIT处理,但是如果GetValue()内联,JIT将会看到这就像(84 > 100){…很多代码…},则整个块将被删除。幸运的是,这样一个简单的方法几乎总是会自动内联,但是ImmutableArray的GetEnumerator足够大,JIT无法自动识别它的好处。在实践中,当内联GetEnumerator时,JIT最终能够更好地识别出foreach在遍历数组,而不是为Sum生成代码:

; Program.Sum() push rsi sub rsp,30 xor eax,eax mov [rsp+20],rax mov [rsp+28],rax xor esi,esi cmp [rcx],ecx add rcx,8 lea rdx,[rsp+20] call System.Collections.Immutable.ImmutableArray'1[[System.Int32, System.Private.CoreLib]].GetEnumerator() jmp short M00_L01 M00_L00: cmp [rsp+28],edx jae short M00_L02 mov rax,[rsp+20] mov edx,[rsp+28] movsxd rdx,edx mov eax,[rax+rdx*4+10] add esi,eax M00_L01: mov eax,[rsp+28] inc eax mov [rsp+28],eax mov rdx,[rsp+20] mov edx,[rdx+8] cmp edx,eax jg short M00_L00 mov eax,esi add rsp,30 pop rsi ret M00_L02: call CORINFO_HELP_RNGCHKFAIL int 3 ; Total bytes of code 97


就像在.NET Core 3.1中一样,在.NET 5中也是如此

; Program.Sum() sub rsp,28 xor eax,eax add rcx,8 mov rdx,[rcx] mov ecx,[rdx+8] mov r8d,0FFFFFFFF jmp short M00_L01 M00_L00: cmp r8d,ecx jae short M00_L02 movsxd r9,r8d mov r9d,[rdx+r9*4+10] add eax,r9d M00_L01: inc r8d cmp ecx,r8d jg short M00_L00 add rsp,28 ret M00_L02: call CORINFO_HELP_RNGCHKFAIL int 3 ; Total bytes of code 59


因此,更小的代码和更快的执行:

private ImmutableArray<int> _array = ImmutableArray.Create(Enumerable.Range(0, 100_000).ToArray()); [Benchmark] public int Sum() { int sum = 0; foreach (int i in _array) sum += i; return sum; } Method Runtime Mean Ratio
Sum   .NET FW 4.8   187.60 us   1.00  
Sum   .NET Core 3.1   187.32 us   1.00  
Sum   .NET 5.0   46.59 us   0.25  


ImmutableList。包含也看到了显著的改进,由于来自@shortspider的dotnet/corefx#40540。Contains是使用ImmutableList的IndexOf方法实现的,这个方法是在它的枚举器上实现的。在幕后ImmutableList今天AVL树,实现自平衡的二叉查找树的一种形式,为了走这样的树,它需要保持一个非平凡的状态,和ImmutableList的枚举器去煞费苦心每个枚举为了避免分配存储。这导致了不小的开销。但是,Contains并不关心列表中元素的确切索引(也不关心找到了可能的多个副本中的哪个副本),只关心它的存在,因此,它可以使用简单的递归树搜索。(因为树是平衡的,所以我们不关心堆栈溢出条件。)

private ImmutableList<int> _list = ImmutableList.Create(Enumerable.Range(0, 1_000).ToArray()); [Benchmark] public int Sum() { int sum = 0; for (int i = 0; i < 1_000; i++) if (_list.Contains(i)) sum += i; return sum; } Method Runtime Mean Ratio
Sum   .NET FW 4.8   22.259 ms   1.00  
Sum   .NET Core 3.1   22.872 ms   1.03  
Sum   .NET 5.0   2.066 ms   0.09  


前面强调的集合改进都是针对通用集合的,即用于开发人员需要存储的任何数据。但并不是所有的集合类型都是这样的:有些更专门用于特定的数据类型,而这样的集合在。net 5中也可以看到性能的改进。位数组就是这样的一个例子,与几个PRs这个释放作出重大改进,以其性能。特别地,来自@Gnbrkm41的dotnet/corefx#41896使用了AVX2和SSE2特性来对BitArray的许多操作进行矢量化(dotnet/runtime#33749随后也添加了ARM64特性):

private bool[] _array; [GlobalSetup] public void Setup() { var r = new Random(42); _array = Enumerable.Range(0, 1000).Select(_ => r.Next(0, 2) == 0).ToArray(); } [Benchmark] public BitArray Create() => new BitArray(_array); Method Runtime Mean Ratio
Create   .NET FW 4.8   1,140.91 ns   1.00  
Create   .NET Core 3.1   861.97 ns   0.76  
Create   .NET 5.0   49.08 ns   0.04  

LINQ

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

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