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