.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

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