.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数组。这个可以一次性写入(单个ulong)来进行性能优化,基准不测:

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_L01
M00_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_L02
M00_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_L01
M00_L02:
       xor       eax,eax
       add       rsp,28
       ret
M00_L03:
       mov       eax,ecx
       add       rsp,28
       ret
M00_L04:
       call      CORINFO_HELP_RNGCHKFAIL
       int       3
; Total bytes of code 98

M00_L00快速路径,M00_L01慢速路径,在M00_L00前面进行了一个判断,如果没有超出数组边界以及其它判断,那么就M00_L01不进行,否则M00_L02进行边界检查。
另外还有一个概念是循环提升,这个就另说了。


结尾

作者:江湖评谈
参照:[微软官方博客]
文章首发于公众号【江湖评谈】,欢迎大家关注。
image

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章