.Net7基礎型別的優化和迴圈克隆優化

2023-06-13 21:06:16

前言

.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進行邊界檢查。
另外還有一個概念是迴圈提升,這個就另說了。


結尾

作者:江湖評談
參照:[微軟官方部落格]
文章首發於公眾號【江湖評談】,歡迎大家關注。