.Net7基础类型的优化和循环克隆优化

前言

.Net7里面对于基础类型的优化,是必不可少的。因为这些基础类型基本上都会经常用到,本篇除了基础类型的优化介绍之外,还有一个循环克隆的优化特性,也一并看下。文章来源:微软官方博客.

概括

1.基础类型优化
基础类型的优化有些不会涉及ASM,主要是记忆。
:double.Parse和float.Parse,把某数值转换成double或者float类型,这两个Parse进行了优化,
:bool.TryParse和bool.TryFormat也进行了性能优化。
假如说有以下代码:

destination[0] = 'T';destination[1] = 'r';destination[2] = 'u';destination[3] = 'e';

四次写操作,写入到destination数组。这个可以一次性写入来进行性能优化:

BinaryPrimitives.WriteUInt64LittleEndian(MemoryMarshal.AsBytes(destination), 0x65007500720054); // "True"0x65007500720054是内存地址,里面存放了四个char的值。
private bool _value = true;private char[] _chars = new char[] { 'T', 'r', 'u', 'e' };
[Benchmark] public bool ParseTrue() => bool.TryParse(_chars, out _);[Benchmark] public bool FormatTrue() => _value.TryFormat(_chars, out _);

:Enum枚举也进行了性能优化

这里主要是二进制算法和线性算法的综合应用,因为当我们执行枚举的一些方法,比如Enum.IsDefined、Enum.GetName或Enum.ToString的时候。

它会搜索一些值,这些值也是存储在数组中的,会使用Array.BinarySearch二进制来搜索。涉及到复杂的算法的时候Array.BinarySearch二进制搜索是可以的,但是如果比较简单的算法则用它相当于杀鸡用牛刀,这里就引入了线性搜索:SpanHelpers.IndexOf。那么何时用线性何时用二进制搜索呢?对于小于或等于32个定义值的枚举用线性,大于的用二进制。
代码如下,benchmark这里就不打印出来了

private DayOfWeek[] _days = Enum.GetValues<DayOfWeek>();[Benchmark]public bool AllDefined(){    foreach (DayOfWeek day in _days)    {        if (!Enum.IsDefined(day))        {            return false;        }    }
    return true;}

Enums在与Nullable和EqualityComparer.Default的配合下也得到了性能提升,因为EqualityComparer.Default缓存了一个从所有对Default的访问中返回的EqualityComparer

实例,EqualityComparer.Default.Equals

根据它这个里面的T值来确保了nullable enums被映射到(现有的)Nullable的专门比较器上,并简单地调整了其定义以确保它能与enums很好地配合。

代码如下,Benchmark不测

private DayOfWeek?[] _enums = Enum.GetValues<DayOfWeek>().Select(e => (DayOfWeek?)e).ToArray();[Benchmark][Arguments(DayOfWeek.Saturday)]public int FindEnum(DayOfWeek value) => IndexOf(_enums, value);private static int IndexOf<T>(T[] values, T value){    for (int i = 0; i < values.Length; i++)    {        if (EqualityComparer<T>.Default.Equals(values[i], value))//这里的T值是IndexOf传过来的,进行的一个性能优化        {            return i;        }    }
    return -1;}

:Guid的优化

Guid实现将数据分成4个32位的值,并进行4个int的比较。如果当前的硬件支持128位SIMD,实现就会将两个Guid的数据加载为两个向量,并简单地进行一次比较。benchmark不测

private Guid _guid1 = Guid.Parse("0aa2511d-251a-4764-b374-4b5e259b6d9a");private Guid _guid2 = Guid.Parse("0aa2511d-251a-4764-b374-4b5e259b6d9a");[Benchmark]public bool GuidEquals() => _guid1 == _guid2;

:DateTime.Equals的优化

DateTime.Equals,DateTime是用一个单一的ulong _dateData字段实现的。其中大部分位存储了从1/1/0001 12:00am开始的ticks偏移量,每个tick是100纳秒,并且前两个位描述了DateTimeKind。因此,公共的Ticks属性返回_dateData的值,但前两位被屏蔽掉了,例如:_dateData & 0x3FFFFFFFFFFFFFFF。然后,平等运算符只是将一个DateTime的Ticks与其他DateTime的Ticks进行比较,这样我们就可以有效地得到

(dt1._dateData & 0x3FFFFFFFFFFF)==(dt2._dateData & 0x3FFFFFFFFFFF)。

然而,作为一个微观的优化,可以更有效地表达为

((dt1._dateData ^ dt2._dateData) << 2) == 0。
这里其实是一个细微的优化,但是依然可见优化力度。

.Net6; Program.DateTimeEquals()       mov       rax,[rcx+8]       mov       rdx,[rcx+10]       mov       rcx,0FFFFFFFFFFFF       and       rax,rcx       and       rdx,rcx       cmp       rax,rdx       sete      al       movzx     eax,al       ret; Total bytes of code 34而在.NET 7上则产生。; Program.DateTimeEquals()       mov       rax,[rcx+8]       mov       rdx,[rcx+10]       xor       rax,rdx       shl       rax,2       sete      al       movzx     eax,al       ret; Total bytes of code 22
所以我们得到的不是mov、and、and、cmp,而是xor和shl。

其它还有一些DateTime.Day、DateTime.DayOfYear、DateTime.DayOfYear改进性能。

:数学API的优化
:System.Formats.Tar压缩文件库的优化

2.循环克隆优化
循环克隆实际上是通过提前判断是否超出数组边界来进行的一个优化,如果没有超过数组边界,则快速路,超过了就慢速路径进行数组边界检查。

private int[] _values = Enumerable.Range(0, 1000).ToArray();[Benchmark][Arguments(0, 0, 1000)]public int LastIndexOf(int arg, int offset, int count){    int[] values = _values;    for (int i = offset + count - 1; i >= offset; i--)        if (values[i] == arg)            return i;    return 0;}

.Net7 ASM

; Program.LastIndexOf(Int32, Int32, Int32)       sub       rsp,28       mov       rax,[rcx+8]       lea       ecx,[r8+r9+0FFFF]       cmp       ecx,r8d       jl        short M00_L02       test      rax,rax       je        short M00_L01       test      ecx,ecx       jl        short M00_L01       test      r8d,r8d       jl        short M00_L01       cmp       [rax+8],ecx       jle       short M00_L01M00_L00:       mov       r9d,ecx       cmp       [rax+r9*4+10],edx       je        short M00_L03       dec       ecx       cmp       ecx,r8d       jge       short M00_L00       jmp       short M00_L02M00_L01:       cmp       ecx,[rax+8]       jae       short M00_L04       mov       r9d,ecx       cmp       [rax+r9*4+10],edx       je        short M00_L03       dec       ecx       cmp       ecx,r8d       jge       short M00_L01M00_L02:       xor       eax,eax       add       rsp,28       retM00_L03:       mov       eax,ecx       add       rsp,28       retM00_L04:       call      CORINFO_HELP_RNGCHKFAIL       int       3; Total bytes of code 98

M00_L00快速路径,M00_L01慢速路径,在M00_L00前面进行了一个判断,如果没有超出数组边界以及其它判断,那么就M00_L01不进行,否则M00_L02进行边界检查。

另外还有一个概念是循环提升,这个就另说了。