1. 程式人生 > >【翻譯】.NET 5中的效能改進

【翻譯】.NET 5中的效能改進

# 【翻譯】.NET 5中的效能改進 在.NET Core之前的版本中,其實已經在部落格中介紹了在該版本中發現的重大效能改進。 從.NET Core 2.0到.NET Core 2.1到.NET Core 3.0的每一篇文章,發現
談論越來越多的東西。 然而有趣的是,每次都想知道下一次是否有足夠的意義的改進以保證再發表一篇文章。 .NET 5已經實現了許多效能改進,儘管直到今年秋天才計劃釋出最終版本,並且到那時很有可能會有更多的改進,但是還要強調一下,現在已提供的改進。 在這篇文章中,重點介紹約250個PR,這些請求為整個.NET 5的效能提升做出了巨大貢獻。
## 安裝
Benchmark.NET現在是衡量.NET程式碼效能的規範工具,可輕鬆分析程式碼段的吞吐量和分配。 因此,本文中大部分示例都是使用使用該工具編寫的微基準來衡量的。首先建立了一個目錄,然後使用dotnet工具對其進行了擴充套件:
``` mkdir Benchmarks cd Benchmarks dotnet new console ```
生成的Benchmarks.csproj的內容擴充套件為如下所示:
```csharp Exe true true net5.0;netcoreapp3.1;net48 ```
這樣,就可以針對.NET Framework 4.8,.NET Core 3.1和.NET 5執行基準測試(目前已為Preview 8安裝了每晚生成的版本)。.csproj還引用Benchmark.NET NuGet軟體包(其最新版本為12.1版),以便能夠使用其功能,然後引用其他幾個庫和軟體包,特別是為了支援能夠在其上執行測試 .NET Framework 4.8。
然後,將生成的Program.cs檔案更新到同一資料夾中,如下所示:
```csharp using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Diagnosers; using BenchmarkDotNet.Running; using System; using System.Buffers.Text; using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Immutable; using System.IO; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Security; using System.Net.Sockets; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; [MemoryDiagnoser] public class Program { static void Main(string[] args) =>
BenchmarkSwitcher.FromAssemblies(new[] { typeof(Program).Assembly }).Run(args); // BENCHMARKS GO HERE } ```
對於每次測試,每個示例中顯示的基準程式碼複製/貼上將顯示`"// BENCHMARKS GO HERE"`的位置。
為了執行基準測試,然後做:
```bash dotnet run -c Release -f net48 --runtimes net48 netcoreapp31 netcoreapp50 --filter ** --join ```
這告訴Benchmark.NET:
- 使用.NET Framework 4.8 來建立基準。 - 針對.NET Framework 4.8,.NET Core 3.1和.NET 5分別執行基準測試。 - 在程式集中包含所有基準測試(不要過濾掉任何基準測試)。 - 將所有基準測試的輸出結果合併在一起,並在執行結束時顯示(而不是貫穿整個過程)。
在某些情況下,針對特定目標的API並不存在,我只是省略了命令列的這一部分。

最後,請注意以下幾點:
- 從執行時和核心庫的角度來看,它與幾個月前釋出的前身相比沒有多少改進。 但是,還進行了一些改進,在某些情況下,目前已經將.NET 5的改進移植回了.NET Core 3.1,在這些改進中,這些更改被認為具有足夠的影響力,可以保證可以新增到長期支援中(LTS)版本。 因此,我在這裡所做的所有比較都是針對最新的.NET Core 3.1服務版本(3.1.5),而不是針對.NET Core 3.0。 - 由於比較是關於.NET 5與.NET Core 3.1的,而且.NET Core 3.1不包括mono執行時,因此不討論對mono所做的改進,也沒有專門針對[“Blazor”](https://devblogs.microsoft.com/aspnet/blazor-webassembly-3-2-0-now-available/)。 因此,當指的是“runtime”時,指的是coreclr,即使從.NET 5開始,它也包含多個執行時,並且所有這些都已得到改進。 - 大多數示例都在Windows上執行,因為也希望能夠與.NET Framework 4.8進行比較。 但是,除非另有說明,否則所有顯示的示例均適用於Windows,Linux和macOS。 - 需要注意的是: 這裡的所有測量資料都是在的桌上型電腦上進行的,測量結果可能會有所不同。微基準測試對許多因素都非常敏感,包括處理器數量、處理器架構、記憶體和快取速度等等。但是,一般來說,我關注的是效能改進,幷包含了通常能夠承受此類差異的示例。
讓我們開始吧…

## GC
對於所有對.NET和效能感興趣的人來說,垃圾收集通常是他們最關心的。在減少分配上花費了大量的精力,不是因為分配行為本身特別昂貴,而是因為通過垃圾收集器(GC)清理這些分配之後的後續成本。然而,無論減少分配需要做多少工作,絕大多數工作負載都會導致這種情況發生,因此,重要的是要不斷提高GC能夠完成的任務和速度。

這個版本在改進GC方面做了很多工作。例如, [dotnet/coreclr#25986](https://github.com/dotnet/coreclr/pull/25986) 為GC的“mark”階段實現了一種形式的工作竊取。.NET GC是一個[“tracing”](https://en.wikipedia.org/wiki/Tracing_garbage_collection)收集器,這意味著(在非常高的級別上)當它執行時,它從一組“roots”(已知的固有可訪問的位置,比如靜態欄位)開始,從一個物件遍歷到另一個物件,將每個物件“mark”為可訪問;在所有這些遍歷之後,任何沒有標記的物件都是不可訪問的,可以收集。此標記代表了執行集合所花費的大部分時間,並且此PR通過更好地平衡集合中涉及的每個執行緒執行的工作來改進標記效能。當使用“Server GC”執行時,每個核都有一個執行緒參與收集,當執行緒完成分配給它們的標記工作時,它們現在能夠從其他執行緒“steal” 未完成的工作,以幫助更快地完成整個收集。

另一個例子是,[dotnet/runtime#35896](https://github.com/dotnet/runtime/pull/35896) “ephemeral”段的解壓進行了優化(gen0和gen1被稱為 “ephemeral”,因為它們是預期只持續很短時間的物件)。在段的最後一個活動物件之後,將記憶體頁返回給作業系統。那麼GC的問題就變成了,這種解解應該在什麼時候發生,以及在任何時候應該解解多少,因為在不久的將來,它可能需要為額外的分配分配額外的頁面。

或者以[dotnet/runtime#32795](https://github.com/dotnet/runtime/pull/32795),為例,它通過減少在GC靜態掃描中涉及的鎖爭用,提高了在具有較高核心計數的機器上的GC可伸縮性。或者[dotnet/runtime#37894](https://github.com/dotnet/runtime/pull/37894),它避免了代價高昂的記憶體重置(本質上是告訴作業系統相關的記憶體不再感興趣),除非GC看到它處於低記憶體的情況。或者[dotnet/runtime#37159](https://github.com/dotnet/runtime/pull/37159),它(雖然還沒有合併,預計將用於.NET5 )構建在[@damageboy](https://github.com/damageboy)的工作之上,用於向量化GC中使用的排序。或者 [dotnet/coreclr#27729](https://github.com/dotnet/coreclr/pull/27729),它減少了GC掛起執行緒所花費的時間,這對於它獲得一個穩定的檢視,從而準確地確定正在使用的執行緒是必要的。

這只是改進GC本身所做的部分更改,但最後一點給我帶來了一個特別吸引我的話題,因為它涉及到近年來我們在.NET中所做的許多工作。在這個版本中,我們繼續,甚至加快了從C/C++移植coreclr執行時中的本地實現,以取代System.Private.Corelib中的普通c#託管程式碼。此舉有大量的好處,包括讓我們更容易共享一個實現跨多個執行時(如coreclr和mono),甚至對我們來說更容易進化API表面積,如通過重用相同的邏輯來處理陣列和跨越。但讓一些人吃驚的是,這些好處還包括多方面的效能。其中一種方法回溯到使用託管執行時的最初動機:安全性。預設情況下,用c#編寫的程式碼是“safe”,因為執行時確保所有記憶體訪問都檢查了邊界,只有通過程式碼中可見的顯式操作(例如使用unsafe關鍵字,Marshal類,unsafe類等),開發者才能刪除這種驗證。結果,作為一個開源專案的維護人員,我們的工作的航運安全系統在很大程度上使當貢獻託管程式碼的形式:雖然這樣的程式碼可以當然包含錯誤,可能會通過程式碼審查和自動化測試,我們可以晚上睡得更好知道這些bug引入安全問題的機率大大降低。這反過來意味著我們更有可能接受託管程式碼的改進,並且速度更快,貢獻者提供的更快,我們幫助驗證的更快。我們還發現,當使用c#而不是C時,有更多的貢獻者對探索效能改進感興趣,而且更多的人以更快的速度進行實驗,從而獲得更好的效能。

然而,我們從移植中看到了更直接的效能改進。託管程式碼呼叫執行時所需的開銷相對較小,但是如果呼叫頻率很高,那麼開銷就會增加。考慮[dotnet/coreclr#27700](https://github.com/dotnet/coreclr/pull/27700),它將原始型別陣列排序的實現從coreclr的原生代碼移到了Corelib的c#中。除了這些程式碼之外,它還為新的公共api提供了對跨度進行排序的支援,它還降低了對較小陣列進行排序的成本,因為排序的成本主要來自於從託管程式碼的轉換。我們可以在一個小的基準測試中看到這一點,它只是使用陣列。對包含10個元素的int[], double[]和string[]陣列進行排序:
```csharp public class DoubleSorting : Sorting { protected override double GetNext() => _random.Next(); } public class Int32Sorting : Sorting { protected override int GetNext() => _random.Next(); } public class StringSorting : Sorting { protected override string GetNext() { var dest = new char[_random.Next(1, 5)]; for (int i = 0; i < dest.Length; i++) dest[i] = (char)('a' + _random.Next(26)); return new string(dest); } } public abstract class Sorting { protected Random _random; private T[] _orig, _array; [Params(10)] public int Size { get; set; } protected abstract T GetNext(); [GlobalSetup] public void Setup() { _random = new Random(42); _orig = Enumerable.Range(0, Size).Select(_ => GetNext()).ToArray(); _array = (T[])_orig.Clone(); Array.Sort(_array); } [Benchmark] public void Random() { _orig.AsSpan().CopyTo(_array); Array.Sort(_array); } } ``` | Type | Runtime | Mean | Ratio | | --- | --- | --- | --- | | DoubleSorting | .NET FW 4.8 | 88.88 ns | 1.00 | | DoubleSorting | .NET Core 3.1 | 73.29 ns | 0.83 | | DoubleSorting | .NET 5.0 | 35.83 ns | 0.40 | | | | | | | Int32Sorting | .NET FW 4.8 | 66.34 ns | 1.00 | | Int32Sorting | .NET Core 3.1 | 48.47 ns | 0.73 | | Int32Sorting | .NET 5.0 | 31.07 ns | 0.47 | | | | | | | StringSorting | .NET FW 4.8 | 2,193.86 ns | 1.00 | | StringSorting | .NET Core 3.1 | 1,713.11 ns | 0.78 | | StringSorting | .NET 5.0 | 1,400.96 ns | 0.64 |
這本身就是這次遷移的一個很好的好處,因為我們在.NET5中通過[dotnet/runtime#37630](https://github.com/dotnet/runtime/pull/37630) 添加了System.Half,一個新的原始16位浮點,並且在託管程式碼中,這個排序實現的優化幾乎立即應用到它,而以前的本地實現需要大量的額外工作,因為沒有c++標準型別的一半。但是,這裡還有一個更有影響的效能優勢,這讓我們回到我開始討論的地方:GC。

GC的一個有趣指標是“pause time”,這實際上意味著GC必須暫停執行時多長時間才能執行其工作。更長的暫停時間對延遲有直接的影響,而延遲是所有工作負載方式的關鍵指標。正如前面提到的,GC可能需要暫停執行緒為了得到一個一致的世界觀,並確保它能安全地移動物件,但是如果一個執行緒正在執行C/c++程式碼在執行時,GC可能需要等到呼叫完成之前暫停的執行緒。因此,我們在託管程式碼而不是本機程式碼中做的工作越多,GC暫停時間就越好。我們可以使用相同的陣列。排序的例子,看看這個。考慮一下這個程式:
```csharp using System; using System.Diagnostics; using System.Threading; class Program { public static void Main() { new Thread(() => { var a = new int[20]; while (true) Array.Sort(a); }) { IsBackground = true }.Start(); var sw = new Stopwatch(); while (true) { sw.Restart(); for (int i = 0; i < 10; i++) { GC.Collect(); Thread.Sleep(15); } Console.WriteLine(sw.Elapsed.TotalSeconds); } } } ```
這是讓一個執行緒在一個緊密迴圈中不斷地對一個小陣列排序,而在主執行緒上,它執行10次GCs,每次GCs之間大約有15毫秒。我們預計這個迴圈會花費150毫秒多一點的時間。但當我在.NET Core 3.1上執行時,我得到的秒數是這樣的
```csharp 6.6419048 5.5663149 5.7430339 6.032052 7.8892468 ```
在這裡,GC很難中斷執行排序的執行緒,導致GC暫停時間遠遠高於預期。幸運的是,當我在 .NET5 上執行這個時,我得到了這樣的數字:
```csharp 0.159311 0.159453 0.1594669 0.1593328 0.1586566 ```
這正是我們預測的結果。通過移動陣列。將實現排序到託管程式碼中,這樣執行時就可以在需要時更容易地掛起實現,我們使GC能夠更好地完成其工作。

當然,這不僅限於Array.Sort。 一堆PR進行了這樣的移植,例如[dotnet/runtime#32722](https://github.com/dotnet/runtime/pull/32722)將stdelemref和ldelemaref JIT helper 移動到C#,[dotnet/runtime#32353](https://github.com/dotnet/runtime/pull/32353) 將unbox helpers的一部分移動到C#(並使用適當的GC輪詢位置來檢測其餘部分) GC在其餘位置適當地暫停),[dotnet/coreclr#27603](https://github.com/dotnet/coreclr/pull/27603) / [dotnet/coreclr#27634](https://github.com/dotnet/coreclr/pull/27634) / [dotnet/coreclr#27123](https://github.com/dotnet/coreclr/pull/27123) / [dotnet/coreclr#27776](https://github.com/dotnet/coreclr/pull/27776) 移動更多的陣列實現,如Array.Clear和Array.Copy到C#, [dotnet/coreclr#27216](https://github.com/dotnet/coreclr/pull/27216) 將更多Buffer移至C#,而[dotnet/coreclr#27792](https://github.com/dotnet/coreclr/pull/27792)將Enum.CompareTo移至C#。 這些更改中的一些然後啟用了後續增益,例如 [dotnet/runtime#32342](https://github.com/dotnet/runtime/pull/32342)和[dotnet/runtime#35733](https://github.com/dotnet/runtime/pull/35733),它們利用Buffer.Memmove的改進來在各種字串和陣列方法中獲得額外的收益。

關於這組更改的最後一個想法是,需要注意的另一件有趣的事情是,在一個版本中所做的微優化是如何基於後來被證明無效的假設的,並且當使用這種微優化時,需要準備並願意適應。在我的.NET Core 3.0部落格中,我提到了像[dotnet/coreclr#21756](https://github.com/dotnet/coreclr/pull/21756)這樣的“peanut butter”式的改變,它改變了很多使用陣列的呼叫站點。複製(源,目標,長度),而不是使用陣列。複製(source, sourceOffset, destination, destinationOffset, length),因為前者獲取源陣列和目標陣列的下限的開銷是可測量的。但是通過前面提到的將陣列處理程式碼移動到c#的一系列更改,更簡單的過載的開銷消失了,使其成為這些操作更簡單、更快的選擇。這樣,.NET5 PRs [dotnet/coreclr#27641](https://github.com/dotnet/coreclr/pull/27641)和[dotnet/corefx#42343](https://github.com/dotnet/corefx/pull/42343)切換了所有這些呼叫站點,更多地回到使用更簡單的過載。[dotnet/runtime#36304](https://github.com/dotnet/runtime/pull/36304)是另一個取消之前優化的例子,因為更改使它們過時或實際上有害。你總是能夠傳遞一個字元到字串。分裂,如version.Split (' . ')。然而,問題是,這個繫結到Split的唯一過載是Split(params char[] separator),這意味著每次這樣的呼叫都會導致c#編譯器生成一個char[]分配。為了解決這個問題,以前的版本添加了快取,提前分配陣列並將它們儲存到靜態中,然後可以被分割呼叫使用,以避免每個呼叫都使用char[]。既然.NET中有一個Split(char separator, StringSplitOptions options = StringSplitOptions. none)過載,我們就不再需要陣列了。
作為最後一個示例,我展示了將程式碼移出執行時並轉移到託管程式碼中如何幫助GC暫停,但是當然還有其他方式可以使執行時中剩餘的程式碼對此有所幫助。[dotnet/runtime#36179](https://github.com/dotnet/runtime/pull/36179)通過確保執行時處於程式碼[爭搶模式](https://github.com/dotnet/runtime/blob/4fdf9ff8812869dcf957ce0d2eb07c0d5779d1c6/docs/coding-guidelines/clr-code-guide.md#218-use-the-right-gc-mode--preemptive-vs-cooperative)下(例如獲取“Watson”儲存桶引數(基本上是一組用於唯一標識此特定異常和呼叫堆疊以用於報告目的的資料)),從而減少了由於異常處理而導致的GC暫停。 。暫停。 ## JIT
.NET5 也是即時(JIT)編譯器的一個令人興奮的版本,該版本中包含了各種各樣的改進。與任何編譯器一樣,對JIT的改進可以產生廣泛的影響。通常,單獨的更改對單獨的程式碼段的影響很小,但是這樣的更改會被它們應用的地方的數量放大。
可以向JIT新增的優化的數量幾乎是無限的,如果給JIT無限的時間來執行這種優化,JIT就可以為任何給定的場景建立最優程式碼。但是JIT的時間並不是無限的。JIT的“即時”特性意味著它在應用程式執行時執行編譯:當呼叫尚未編譯的方法時,JIT需要按需為其提供彙編程式碼。這意味著在編譯完成之前執行緒不能向前推進,這反過來意味著JIT需要在應用什麼優化以及如何選擇使用有限的時間預算方面有策略。各種技術用於給JIT更多的時間,比如使用“提前”(AOT)編譯應用程式的一些部分做盡可能多的編譯工作前儘可能執行應用程式(例如,AOT編譯核心庫都使用一個叫[“ReadyToRun”](https://github.com/dotnet/runtime/blob/99aae90739c2ad5642a36873334c82a8b7fb2de9/docs/design/coreclr/botr/readytorun-overview.md)的技術,你可能會聽到稱為“R2R”甚至“crossgen”,是產生這些影象的工具),或使用[“tiered compilation”](https://github.com/dotnet/runtime/blob/9900dfb4b2e32cf02ca846adaf11e93211629ede/docs/design/features/tiered-compilation.md),它允許JIT在最初編譯一個應用了從少到少優化的方法,因此速度非常快,只有在它被認為有價值的時候(即該方法被重複使用的時候),才會花更多的時間使用更多優化來重新編譯它。然而,更普遍的情況是,參與JIT的開發人員只是選擇使用分配的時間預算進行優化,根據開發人員編寫的程式碼和他們使用的程式碼模式,這些優化被證明是有價值的。這意味著,隨著.NET的發展並獲得新的功能、新的語言特性和新的庫特性,JIT也會隨著適合於編寫的較新的程式碼風格的優化而發展。
一個很好的例子是[@benaadams](https://github.com/benaadams)的[dotnet/runtime#32538](https://github.com/dotnet/runtime/pull/32538)。 Span 一直滲透到.NET堆疊的所有層,因為從事執行時,核心庫,ASP.NET Core的開發人員以及其他人在編寫安全有效的程式碼(也統一了字串處理)時認識到了它的強大功能 ,託管陣列,本機分配的記憶體和其他形式的資料。 類似地,值型別(結構)被越來越普遍地用作通過堆疊分配避免物件分配開銷的一種方式。 但是,對此類型別的嚴重依賴也給執行時帶來了更多麻煩。 coreclr執行時使用[“precise” garbage collector](https://en.wikipedia.org/wiki/Tracing_garbage_collection#Precise_vs._conservative_and_internal_pointers),這意味著GC能夠100%準確地跟蹤哪些值引用託管物件,哪些值不引用託管物件; 這樣做有好處,但也有代價(相反,mono執行時使用“conservative”垃圾收集器,這具有一些效能上的好處,但也意味著它可以解釋堆疊上的任意值,而該值恰好與 被管理物件的地址作為對該物件的實時引用)。 這樣的代價之一是,JIT需要通過確保在GC注意之前將任何可以解釋為物件引用的區域性都清零來幫助GC。 否則,GC可能最終會在尚未設定的本地中看到一個垃圾值,並假定它引用的是有效物件,這時可能會發生“bad things”。 參考當地人越多,需要進行的清理越多。 如果您只清理一些當地人,那可能不會引起注意。 但是隨著數量的增加,清除這些本地物件所花費的時間可能加起來,尤其是在非常熱的程式碼路徑中使用的一種小方法中。 這種情況在跨度和結構中變得更加普遍,在這種情況下,編碼模式通常會導致需要為零的更多引用(Span 包含引用)。 前面提到的PR通過更新JIT生成的序號塊的程式碼來解決此問題,這些序號塊使用xmm暫存器而不是rep stosd指令來執行該清零操作。 有效地,它對歸零進行向量化處理。 您可以通過以下基準測試看到此影響:
```csharp [Benchmark] public int Zeroing() { ReadOnlySpan s1 = "hello world"; ReadOnlySpan s2 = Nop(s1); ReadOnlySpan s3 = Nop(s2); ReadOnlySpan s4 = Nop(s3); ReadOnlySpan s5 = Nop(s4); ReadOnlySpan s6 = Nop(s5); ReadOnlySpan s7 = Nop(s6); ReadOnlySpan s8 = Nop(s7); ReadOnlySpan s9 = Nop(s8); ReadOnlySpan s10 = Nop(s9); return s1.Length + s2.Length + s3.Length + s4.Length + s5.Length + s6.Length + s7.Length + s8.Length + s9.Length + s10.Length; } [MethodImpl(MethodImplOptions.NoInlining)] private static ReadOnlySpan Nop(ReadOnlySpan span) => default; ```
在我的機器上,我得到如下結果:
| Method | Runtime | Mean | Ratio | | --- | --- | --- | --- | | Zeroing | .NET FW 4.8 | 22.85 ns | 1.00 | | Zeroing | .NET Core 3.1 | 18.60 ns | 0.81 | | Zeroing | .NET 5.0 | 15.07 ns | 0.66 |
請注意,這種零實際上需要在比我提到的更多的情況下。特別是,預設情況下,c#規範要求在執行開發人員的程式碼之前,將所有本地變數初始化為預設值。你可以通過這樣一個例子來了解這一點:
```csharp using System; using System.Runtime.CompilerServices; using System.Threading; unsafe class Program { static void Main() { while (true) { Example(); Thread.Sleep(1); } } [MethodImpl(MethodImplOptions.NoInlining)] static void Example() { Guid g; Console.WriteLine(*&g); } } ```
執行它,您應該只看到所有0輸出的guid。這是因為c#編譯器在編譯的示例方法的IL中發出一個.locals init標誌,而.locals init告訴JIT它需要將所有的區域性變數歸零,而不僅僅是那些包含引用的區域性變數。然而,在.NET 5中,執行時中有一個新屬性([dotnet/runtime#454](https://github.com/dotnet/runtime/pull/454)):
```csharp namespace System.Runtime.CompilerServices { [AttributeUsage(AttributeTargets.Module | AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Constructor | AttributeTargets.Method | AttributeTargets.Property | AttributeTargets.Event | AttributeTargets.Interface, Inherited = false)] public sealed class SkipLocalsInitAttribute : Attribute { } } ```
c#編譯器可以識別這個屬性,它用來告訴編譯器在其他情況下不發出.locals init。如果我們對前面的示例稍加修改,就可以將屬性新增到整個模組中:
```csharp using System; using System.Runtime.CompilerServices; using System.Threading; [module: SkipLocalsInit] unsafe class Program { static void Main() { while (true) { Example(); Thread.Sleep(1); } } [MethodImpl(MethodImplOptions.NoInlining)] static void Example() { Guid g; Console.WriteLine(*&g); } } ```
現在應該會看到不同的結果,特別是很可能會看到非零的guid。在[dotnet/runtime#37541](https://github.com/dotnet/runtime/pull/37541)中,.NET5 中的核心庫現在都使用這個屬性來禁用.locals init(在以前的版本中,.locals init在構建核心庫時通過編譯後的一個步驟刪除)。請注意,c#編譯器只允許在不安全的上下文中使用SkipLocalsInit,因為它很容易導致未經過適當驗證的程式碼損壞(因此,如果/當您應用它時,請三思)。

除了使零的速度更快,也有改變,以消除零完全。例如,[dotnet/runtime#31960](https://github.com/dotnet/runtime/pull/31960), [dotnet/runtime#36918](https://github.com/dotnet/runtime/pull/36918), [dotnet/runtime#37786](https://github.com/dotnet/runtime/pull/37786),和[dotnet/runtime#38314](https://github.com/dotnet/runtime/pull/38314) 都有助於消除零,當JIT可以證明它是重複的。
這樣的零是託管程式碼的一個例子,執行時需要它來保證其模型和上面語言的需求。另一種此類稅收是邊界檢查。使用託管程式碼的最大優勢之一是,在預設情況下,整個類的潛在安全漏洞都變得無關緊要。執行時確保陣列、字串和span的索引被檢查,這意味著執行時注入檢查以確保被請求的索引在被索引的資料的範圍內(即greather大於或等於0,小於資料的長度)。這裡有一個簡單的例子:
```csharp public static char Get(string s, int i) => s[i]; ```
為了保證這段程式碼的安全,執行時需要生成一個檢查,檢查i是否在字串s的範圍內,這是JIT通過如下程式集完成的:
```csharp ; Program.Get(System.String, Int32) sub rsp,28 cmp edx,[rcx+8] jae short M01_L00 movsxd rax,edx movzx eax,word ptr [rcx+rax*2+0C] add rsp,28 ret M01_L00: call CORINFO_HELP_RNGCHKFAIL int 3 ; Total bytes of code 28 ```
這個程式集是通過Benchmark的一個方便特性生成的。將[DisassemblyDiagnoser]新增到包含基準測試的類中,它就會吐出被分解的彙編程式碼。我們可以看到,大會將字串(通過rcx暫存器)和載入字串的長度(8個位元組儲存到物件,因此,[rcx + 8]),與我經過比較,edx登記,如果與一個無符號的比較(無符號,這樣任何負環繞大於長度)我是長度大於或等於,跳到一個輔助COREINFO_HELP_RNGCHKFAIL丟擲一個異常。只有幾條指令,但是某些型別的程式碼可能會花費大量的迴圈索引,因此,當JIT可以消除儘可能多的不必要的邊界檢查時,這是很有幫助的。
JIT已經能夠在各種情況下刪除邊界檢查。例如,當你寫迴圈:
```csharp int[] arr = ...; for (int i = 0; i < arr.Length; i++) Use(arr[i]); ```
JIT可以證明我永遠不會超出陣列的邊界,因此它可以省略它將生成的邊界檢查。在.NET5 中,它可以在更多的地方刪除邊界檢查。例如,考慮這個函式,它將一個整數的位元組作為字元寫入一個span:
```csharp private static bool TryToHex(int value, Span span) { if ((uint)span.Length <= 7) return false; ReadOnlySpan map = new byte[] { (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F' }; ; span[0] = (char)map[(value >> 28) & 0xF]; span[1] = (char)map[(value >> 24) & 0xF]; span[2] = (char)map[(value >> 20) & 0xF]; span[3] = (char)map[(value >> 16) & 0xF]; span[4] = (char)map[(value >> 12) & 0xF]; span[5] = (char)map[(value >> 8) & 0xF]; span[6] = (char)map[(value >> 4) & 0xF]; span[7] = (char)map[value & 0xF]; return true; } private char[] _buffer = new char[100]; [Benchmark] public bool BoundsChecking() => TryToHex(int.MaxValue, _buffer); ```
首先,在這個例子中,值得注意的是我們依賴於c#編譯器的優化。注意:
```csharp ReadOnlySpan map = new byte[] { (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F' }; ```
這看起來非常昂貴,就像我們在每次呼叫TryToHex時都要分配一個位元組陣列。事實上,它並不是這樣的,它實際上比我們做的更好:
```csharp private static readonly byte[] s_map = new byte[] { (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', (byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F' }; ... ReadOnlySpan map = s_map; ```
C#編譯器可以識別直接分配給ReadOnlySpan的新位元組陣列的模式(它也可以識別sbyte和bool,但由於位元組關係,沒有比位元組大的)。因為陣列的性質被span完全隱藏了,C#編譯器通過將位元組實際儲存到程式集的資料部分而發出這些位元組,而span只是通過將靜態資料和長度的指標包裝起來而建立的:
```csharp IL_000c: ldsflda valuetype ''/'__StaticArrayInitTypeSize=16' ''::'2125B2C332B1113AAE9BFC5E9F7E3B4C91D828CB942C2DF1EEB02502ECCAE9E9' IL_0011: ldc.i4.s 16 IL_0013: newobj instance void valuetype [System.Runtime]System.ReadOnlySpan'1::.ctor(void*, int32) ```
由於ldc.i4,這對於本次JIT討論很重要。s16在上面。這就是IL載入16的長度來建立跨度,JIT可以看到這一點。它知道跨度的長度是16,這意味著如果它可以證明訪問總是大於或等於0且小於16的值,它就不需要對訪問進行邊界檢查。[dotnet/runtime#1644](https://github.com/dotnet/runtime/pull/1644) 就是這樣做的,它可以識別像array[index % const]這樣的模式,並在const小於或等於長度時省略邊界檢查。在前面的TryToHex示例中,JIT可以看到地圖跨長度16,和它可以看到所有的索引到完成& 0 xf,意義最終將所有值在範圍內,因此它可以消除所有的邊界檢查地圖。結合的事實可能已經看到,沒有邊界檢查需要寫進跨度(因為它可以看到前面長度檢查的方法保護所有索引到跨度),和整個方法是在.NET bounds-check-free 5。在我的機器上,這個基準測試的結果如下: | Method | Runtime | Mean | Ratio | Code Size | | --- | --- | --- | --- | --- | | BoundsChecking | .NET FW 4.8 | 14.466 ns | 1.00 | 830 B | | BoundsChecking | .NET Core 3.1 | 4.264 ns | 0.29 | 320 B | | BoundsChecking | .NET 5.0 | 3.641 ns | 0.25 | 249 B |
注意.NET5的執行速度不僅比.NET Core 3.1快15%,我們還可以看到它的彙編程式碼大小小了22%(額外的“Code Size”一欄來自於我在benchmark類中添加了[DisassemblyDiagnoser])。
另一個很好的邊界檢查移除來自[dotnet/runtime#36263](https://github.com/dotnet/runtime/pull/36263)中的[@nathan-moore](https://github.com/nathan-moore)。我提到過,JIT已經能夠刪除非常常見的從0迭代到陣列、字串或span長度的模式的邊界檢查,但是在此基礎上還有一些比較常見的變化,但以前沒有認識到。例如,考慮這個微基準測試,它呼叫一個方法來檢測一段整數是否被排序:
```csharp private int[] _array = Enumerable.Range(0, 1000).ToArray(); [Benchmark] public bool IsSorted() => IsSorted(_array); private static bool IsSorted(ReadOnlySpan span) { for (int i = 0; i < span.Length - 1; i++) if (span[i] > span[i + 1]) return false; return true; } ```
這種與以前識別的模式的微小變化足以防止JIT忽略邊界檢查。現在不是了.NET5在我的機器上可以快20%的執行: | Method | Runtime | Mean | Ratio | Code Size | | --- | --- | --- | --- | --- | | IsSorted | .NET FW 4.8 | 1,083.8 ns | 1.00 | 236 B | | IsSorted | .NET Core 3.1 | 581.2 ns | 0.54 | 136 B | | IsSorted | .NET 5.0 | 463.0 ns | 0.43 | 105 B |
JIT確保對某個錯誤類別進行檢查的另一種情況是空檢查。JIT與執行時協同完成這一任務,JIT確保有適當的指令來引發硬體異常,然後與執行時一起將這些錯誤轉換為.NET異常([這裡](https://github.com/dotnet/runtime/blob/9df02475e09859a8d24852011cf3515f7a665670/src/coreclr/src/vm/excep.cpp#L3073)))。但有時指令只用於null檢查,而不是完成其他必要的功能,而且只要需要的null檢查是由於某些指令發生的,不必要的重複指令可以被刪除。考慮這段程式碼:
```csharp private (int i, int j) _value; [Benchmark] public int NullCheck() => _value.j++; ```
作為一個可執行的基準測試,它所做的工作太少,無法用基準測試進行準確的度量.NET,但這是檢視生成的彙編程式碼的好方法。在.NET Core 3.1中,此方法產生如下assembly:
```csharp ; Program.NullCheck() nop dword ptr [rax+rax] cmp [rcx],ecx add rcx,8 add rcx,4 mov eax,[rcx] lea edx,[rax+1] mov [rcx],edx ret ; Total bytes of code 23 ``` cmp [rcx],ecx指令在計算j的地址時執行null檢查,然後mov eax,[rcx]指令執行另一個null檢查,作為取消引用j的位置的一部分。因此,第一個null檢查實際上是不必要的,因為該指令沒有提供任何其他好處。所以,多虧了像[dotnet/runtime#1735](https://github.com/dotnet/runtime/pull/1735)和[dotnet/runtime#32641](https://github.com/dotnet/runtime/pull/32641)這樣的PRs,這樣的重複被JIT比以前更多地識別,對於.NET 5,我們現在得到了: ```csharp ; Program.NullCheck() add rcx,0C mov eax,[rcx] lea edx,[rax+1] mov [rcx],edx ret ; Total bytes of code 12 ``` 協方差是JIT需要注入檢查以確保開發人員不會意外地破壞型別或記憶體安全性的另一種情況。考慮一下程式碼 ```csharp class A { } class B { } object[] arr = ...; arr[0] = new A(); ``` 這個程式碼有效嗎?視情況而定。.NET中的陣列是“協變”的,這意味著我可以傳遞一個數組派生型別[]作為BaseType[],其中派生型別派生自BaseType。這意味著在本例中,arr可以被構造為新A[1]或新物件[1]或新B[1]。這段程式碼應該在前兩個中執行良好,但如果arr實際上是一個B[],試圖儲存一個例項到其中必須失敗;否則,使用陣列作為B[]的程式碼可能嘗試使用B[0]作為B,事情可能很快就會變得很糟糕。因此,執行時需要通過協方差檢查來防止這種情況發生,這實際上意味著當引用型別例項儲存到陣列中時,執行時需要檢查所分配的型別實際上與陣列的具體型別相容。使用[dotnet/runtime#189](https://github.com/dotnet/runtime/pull/189), JIT現在能夠消除更多的協方差檢查,特別是在陣列的元素型別是密封的情況下,比如string。因此,像這樣的微基準現在執行得更快了: ```csharp private string[] _array = new string[1000]; [Benchmark] public void CovariantChecking() { string[] array = _array; for (int i = 0; i < array.Length; i++) array[i] = "default"; } ``` | Method | Runtime | Mean | Ratio | Code Size | | --- | --- | --- | --- | --- | | CovariantChecking | .NET FW 4.8 | 2.121 us | 1.00 | 57 B | | CovariantChecking | .NET Core 3.1 | 2.122 us | 1.00 | 57 B | | CovariantChecking | .NET 5.0 | 1.666 us | 0.79 | 52 B |
與此相關的是型別檢查。我之前提到過Span解決了很多問題,但也引入了新的模式,從而推動了系統其他領域的改進;對於Span本身的實現也是這樣。 Span 建構函式做協方差檢查,要求T[]實際上是T[]而不是U[],其中U源自T,例如: ```csharp using System; class Program { static void Main() => new Span(new B[42]); } class A { } class B : A { } ``` [將導致異常:]() ```csharp System.ArrayTypeMismatchException: Attempted to access an element as a type incompatible with the array ```
[該異常源於對Span 的建構函式的]()[檢查](https://github.com/dotnet/runtime/blob/f170db722be6fb695ca229bcbe46be0caa8b3a48/src/libraries/System.Private.CoreLib/src/System/Span.cs#L46-L47): ```csharp if (!typeof(T).IsValueType && array.GetType() != typeof(T[])) ThrowHelper.ThrowArrayTypeMismatchException(); ``` PR [dotnet/runtime#32790](https://github.com/dotnet/runtime/pull/32790)就是這樣優化陣列的.GetType()!= typeof(T [])檢查何時密封T,而[dotnet/runtime#1157](https://github.com/dotnet/runtime/pull/1157)識別typeof(T).IsValueType模式並將其替換為常量 值(PR [dotnet/runtime#1195](https://github.com/dotnet/runtime/pull/1195)對於typeof(T1).IsAssignableFrom(typeof(T2))進行了相同的操作)。 這樣做的最終結果是極大地改善了微基準,例如: ```csharp class A { } sealed class B : A { } private B[] _array = new B[42]; [Benchmark] public int Ctor() => new Span(_array).Length; ``` **我得到的結果如下:** | Method | Runtime | Mean | Ratio | Code Size | | --- | --- | --- | --- | --- | | Ctor | .NET FW 4.8 | 48.8670 ns | 1.00 | 66 B | | Ctor | .NET Core 3.1 | 7.6695 ns | 0.16 | 66 B | | Ctor | .NET 5.0 | 0.4959 ns | 0.01 | 17 B |
當檢視生成的程式集時,差異的解釋就很明顯了,即使不是完全精通程式集程式碼。以下是[DisassemblyDiagnoser]在.NET Core 3.1上生成的內容: ```csharp ; Program.Ctor() push rdi push rsi sub rsp,28 mov rsi,[rcx+8] test rsi,rsi jne short M00_L00 xor eax,eax jmp short M00_L01 M00_L00: mov rcx,rsi call System.Object.GetType() mov rdi,rax mov rcx,7FFE4B2D18AA call CORINFO_HELP_TYPEHANDLE_TO_RUNTIMETYPE cmp rdi,rax jne short M00_L02 mov eax,[rsi+8] M00_L01: add rsp,28 pop rsi pop rdi ret M00_L02: call System.ThrowHelper.ThrowArrayTypeMismatchException() int 3 ; Total bytes of code 66 ``` 下面是.NET5的內容: ```csharp ; Program.Ctor() mov rax,[rcx+8] test rax,rax jne short M00_L00 xor eax,eax jmp short M00_L01 M00_L00: mov eax,[rax+8] M00_L01: ret ; Total bytes of code 17 ``` 另一個例子是,在前面的GC討論中,我提到了將本地執行時程式碼移植到c#程式碼中所帶來的一些好處。有一點我之前沒有提到,但現在將會提到,那就是它導致了我們對系統進行了其他改進,解決了移植的關鍵阻滯劑,但也改善了許多其他情況。一個很好的例子是[dotnet/runtime#38229](https://github.com/dotnet/runtime/pull/38229)。當我們第一次將本機陣列排序實現移動到managed時,我們無意中導致了浮點值的迴歸,這個迴歸被[@nietras](https://github.com/nietras) 發現,隨後在[dotnet/runtime#37941](https://github.com/dotnet/runtime/pull/37941)中修復。迴歸是由於本機實現使用一個特殊的優化,我們失蹤的管理埠(浮點陣列,將所有NaN值陣列的開始,後續的比較操作可以忽略NaN)的可能性,我們成功了。然而,問題是這個的方式表達並沒有導致大量的程式碼重複:本機實現模板,使用和管理實現使用泛型,但限制與泛型等,內聯 helpers介紹,以避免大量的程式碼重複導致non-inlineable在每個比較採用那種方法呼叫。PR [dotnet/runtime#38229](https://github.com/dotnet/runtime/pull/38229)通過允許JIT在同一型別內嵌共享泛型程式碼解決了這個問題。考慮一下這個微基準測試: ```csharp private C c1 = new C() { Value = 1 }, c2 = new C() { Value = 2 }, c3 = new C() { Value = 3 }; [Benchmark] public int Compare() => Comparer.Smallest(c1, c2, c3); class Comparer where T : IComparable { public static int Smallest(T t1, T t2, T t3) => Compare(t1, t2) <= 0 ? (Compare(t1, t3) <= 0 ? 0 : 2) : (Compare(t2, t3) <= 0 ? 1 : 2); [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int Compare(T t1, T t2) => t1.CompareTo(t2); } class C : IComparable { public int Value; public int CompareTo(C other) => other is null ? 1 : Value.CompareTo(other.Value); } ``` 最小的方法比較提供的三個值並返回最小值的索引。它是泛型型別上的一個方法,它呼叫同一型別上的另一個方法,這個方法反過來呼叫泛型型別引數例項上的方法。由於基準使用C作為泛型型別,而且C是引用型別,所以JIT不會專門為C專門化此方法的程式碼,而是使用它生成的用於所有引用型別的“shared”實現。為了讓Compare方法隨後呼叫到CompareTo的正確介面實現,共享泛型實現使用了一個從泛型型別對映到正確目標的字典。在. net的早期版本中,包含那些通用字典查詢的方法是不可行的,這意味著這個最小的方法不能內聯它所做的三個比較呼叫,即使Compare被歸為methodimploptions .侵略化的內聯。前面提到的PR消除了這個限制,在這個例子中產生了一個非常可測量的加速(並使陣列排序迴歸修復可行): | Method | Runtime | Mean | Ratio | | --- | --- | --- | --- | | Compare | .NET FW 4.8 | 8.632 ns | 1.00 | | Compare | .NET Core 3.1 | 9.259 ns | 1.07 | | Compare | .NET 5.0 | 5.282 ns | 0.61 |
這裡提到的大多數改進都集中在吞吐量上,JIT產生的程式碼執行得更快,而更快的程式碼通常(儘管不總是)更小。從事JIT工作的人們實際上非常關注程式碼大小,在許多情況下,將其作為判斷更改是否有益的主要指標。更小的程式碼並不總是更快的程式碼(可以是相同大小的指令,但開銷不同),但從高層次上來說,這是一個合理的度量,更小的程式碼確實有直接的好處,比如對指令快取的影響更小,需要載入的程式碼更少,等等。在某些情況下,更改完全集中在減少程式碼大小上,比如在出現不必要的重複的情況下。考慮一下這個簡單的基準: ```csharp private int _offset = 0; [Benchmark] public int Throw helpers() { var arr = new int[10]; var s0 = new Span(arr, _offset, 1); var s1 = new Span(arr, _offset + 1, 1); var s2 = new Span(arr, _offset + 2, 1); var s3 = new Span(arr, _offset + 3, 1); var s4 = new Span(arr, _offset + 4, 1); var s5 = new Span(arr, _offset + 5, 1); return s0[0] + s1[0] + s2[0] + s3[0] + s4[0] + s5[0]; } ```
Span建構函式[引數驗證](https://github.com/dotnet/runtime/blob/932098fe90d146a73ebd86a2e595398b63b1a600/src/libraries/System.Private.CoreLib/src/System/Span.cs#L68-L80),,T是一個值型別時,結果有兩個叫網站ThrowHelper類上的方法,一個扔一個失敗的null檢查時丟擲的輸入陣列和一個偏移量和計數的範圍(像ThrowArgumentNullException ThrowHelper包含non-inlinable方法,其中包含實際的扔,避免了相關程式碼大小在每個呼叫網站;JIT目前還不能“outlining”(與“inlining”相反),因此需要在重要的情況下手工完成)。在上面的示例中,我們建立了6個Span,這意味著對Span建構函式的6次呼叫,所有這些呼叫都將內聯。JIT陣列為空,所以它可以消除零檢查和ThrowArgumentNullException內聯程式碼,但是它不知道是否偏移量和計算範圍內,因此它需要保留ThrowHelper範圍檢查和呼叫站點。ThrowArgumentOutOfRangeException方法。在.NET Core 3.1中,這個Throw helpers方法生成了如下程式碼:
```csharp M00_L00: call System.ThrowHelper.ThrowArgumentOutOfRangeException() int 3 M00_L01: call System.ThrowHelper.ThrowArgumentOutOfRangeException() int 3 M00_L02: call System.ThrowHelper.ThrowArgumentOutOfRangeException() int 3 M00_L03: call System.ThrowHelper.ThrowArgumentOutOfRangeException() int 3 M00_L04: call System.ThrowHelper.ThrowArgumentOutOfRangeException() int 3 M00_L05: call System.ThrowHelper.ThrowArgumentOutOfRangeException() int 3 ```
在.NET 5中,感謝[dotnet/coreclr#27113](https://github.com/dotnet/coreclr/pull/27113), JIT能夠識別這種重複,而不是所有的6個呼叫站點,它將最終合併成一個: ```csharp M00_L00: call System.ThrowHelper.ThrowArgumentOutOfRangeException() int 3 ``` 所有失敗的檢查都跳到這個共享位置,而不是每個都有自己的副本 | Method | Runtime | Code Size | | --- | --- | --- | | Throw helpers | .NET FW 4.8 | 424 B | | Throw helpers | .NET Core 3.1 | 252 B | | Throw helpers | .NET 5.0 | 222 B |
這些只是.NET 5中對JIT進行的眾多改進中的一部分。還有許多其他改進。[dotnet/runtime#32368](https://github.com/dotnet/runtime/pull/32368)導致JIT將陣列的長度視為無符號,這使得JIT能夠對在長度上執行的某些數學運算(例如除法)使用更好的指令。 [dotnet/runtime#25458](https://github.com/dotnet/runtime/pull/25458) 使JIT可以對某些無符號整數運算使用更快的基於0的比較。 當開發人員實際編寫> = 1時,使用等於!= 0的值。[dotnet/runtime#1378](https://github.com/dotnet/runtime/pull/1378)允許JIT將“ constantString” .Length識別為常量值。 [dotnet/runtime#26740](https://github.com/dotnet/coreclr/pull/26740) 通過刪除nop填充來減小ReadyToRun影象的大小。 [dotnet/runtime#330234](https://github.com/dotnet/runtime/pull/33024)使用加法而不是乘法來優化當x為浮點數或雙精度數時執行x * 2時生成的指令。[dotnet/runtime#27060](https://github.com/dotnet/coreclr/pull/27060)改進了為Math.FusedMultiplyAdd內部函式生成的程式碼。 [dotnet/runtime#27384](https://github.com/dotnet/coreclr/pull/27384)通過使用比以前更好的籬笆指令使ARM64上的易失性操作便宜,並且[dotnet/runtime#38179](https://github.com/dotnet/runtime/pull/38179)在ARM64上執行窺視孔優化以刪除大量冗餘mov指令。 等等。

JIT中還有一些預設禁用的重要更改,目的是獲得關於它們的真實反饋,並能夠在預設情況下post-啟用它們。淨5。例如,[dotnet/runtime#32969](https://github.com/dotnet/runtime/pull/32969)提供了“On Stack Replacement”(OSR)的初始實現。我在前面提到了分層編譯,它使JIT能夠首先為一個方法生成優化最少的程式碼,然後當該方法被證明是重要的時,用更多的優化重新編譯該方法。這允許程式碼執行得更快,並且只有在執行時才升級有效的方法,從而實現更快的啟動時間。但是,分層編譯依賴於替換實現的能力,下次呼叫它時,將呼叫新的實現。但是長時間執行的方法呢?對於包含迴圈(或者,更具體地說,向後分支)的方法,分層編譯在預設情況下是禁用的,因為它們可能會執行很長時間,以至於無法及時使用替換。OSR允許方法在執行程式碼時被更新,而它們是“在堆疊上”的;PR中包含的設計文件中有很多細節(也與分層編譯有關,[dotnet/runtime#1457](https://github.com/dotnet/runtime/pull/1457)改進了呼叫計數機制,分層編譯通過這種機制決定哪些方法應該重新編譯以及何時重新編譯)。您可以通過將COMPlus_TC_QuickJitForLoops和COMPlus_TC_OnStackReplacement環境變數設定為1來試驗OSR。另一個例子是,[dotnet/runtime#1180](https://github.com/dotnet/runtime/pull/1180) 改進了try塊內程式碼的生成程式碼質量,使JIT能夠在暫存器中儲存以前不能儲存的值。您可以通過將COMPlus_EnableEHWriteThr環境變數設定為1來進行試驗。

還有一堆等待拉請求JIT尚未合併,但很可能在.NET 5釋出(除此之外,我預計還有更多在.NET 5釋出之前還沒有釋出的內容)。例如,[dotnet/runtime#32716](https://github.com/dotnet/runtime/pull/32716)允許JIT替換一些分支比較,如a == 42 ?3: 2無分支實現,當硬體無法正確預測將採用哪個分支時,可以幫助提高效能。或[dotnet/runtime#37226](https://github.com/dotnet/runtime/pull/37226),它允許JIT採用像“hello”[0]這樣的模式並將其替換為h;雖然開發人員通常不編寫這樣的程式碼,但在涉及內聯時,這可以提供幫助,通過將常量字串傳遞給內聯的方法,並將其索引到常量位置(通常在長度檢查之後,由於[dotnet/runtime#1378](https://github.com/dotnet/runtime/pull/1378),長度檢查也可以成為常量)。或[dotnet/runtime#1224](https://github.com/dotnet/runtime/pull/1224),它改進了Bmi2的程式碼生成。MultiplyNoFlags內在。或者[dotnet/runtime#37836](https://github.com/dotnet/runtime/pull/37836),它將轉換位操作。將PopCount轉換為一個內因,使JIT能夠識別何時使用常量引數呼叫它,並將整個操作替換為一個預先計算的常量。或[dotnet/runtime#37254](https://github.com/dotnet/runtime/pull/37245),它刪除使用const字串時發出的空檢查。或者來自[@damageboy](https://github.com/damageboy)的[dotnet/runtime#32000](https://github.com/dotnet/runtime/pull/32000) ,它優化了雙重否定。
### Intrinsics
在.NET Core 3.0中,超過1000種新的硬體內建方法被新增並被JIT識別,從而使c#程式碼能夠直接針對指令集,如SSE4和AVX2([docs](https://docs.microsoft.com/en-us/dotnet/api/system.runtime.intrinsics.x86))。然後,在核心庫中的一組api中使用了這些工具。但是,intrinsic僅限於x86/x64架構。在.NET 5中,我們投入了大量的精力來增加數千個元件,特別是針對ARM64,這要感謝眾多貢獻者,特別是來自Arm Holdings的[@TamarChristinaArm](https://github.com/TamarChristinaArm)。與對應的x86/x64一樣,這些內含物在核心庫功能中得到了很好的利用。例如,BitOperations.PopCount()方法之前被優化為使用x86 POPCNT內在的,對於.NET 5,  [dotnet/runtime#35636](https://github.com/dotnet/runtime/pull/35636) 增強了它,使它也能夠使用ARM VCNT或等價的ARM64 CNT。類似地,[dotnet/runtime#34486](https://github.com/dotnet/runtime/pull/34486)修改了位操作。LeadingZeroCount, TrailingZeroCount和Log2利用相應的instrincs。在更高的級別上,來自[@Gnbrkm41](https://github.com/Gnbrkm41)的[dotnet/runtime#33749](https://github.com/dotnet/runtime/pull/33749/)增強了位陣列中的多個方法,以使用ARM64內含物來配合之前新增的對SSE2和AVX2的支援。為了確保Vector api在ARM64上也能很好地執行,我們做了很多工作,比如[dotnet/runtime#33749](https://github.com/dotnet/runtime/pull/33749/)和[dotnet/runtime#36156](https://github.com/dotnet/runtime/pull/36156)。

除ARM64之外,還進行了其他工作以向量化更多操作。 例如,[@Gnbrkm41](https://github.com/Gnbrkm41)還提交了[dotnet/runtime#31993](https://github.com/dotnet/runtime/pull/31993),該檔案利用x64上的ROUNDPS / ROUNDPD和ARM64上的FRINPT / FRINTM來改進為新Vector.Ceiling和Vector.Floor方法生成的程式碼。 BitOperations(這是一種相對低階的型別,針對大多數操作以最合適的硬體內部函式的1:1包裝器的形式實現),不僅在[@saucecontrol](https://github.com/saucecontrol) 的[dotnet/runtime#35650](https://github.com/dotnet/runtime/pull/35650)中得到了改進,而且在Corelib中的使用也得到了改進 更有效率。

最後,JIT進行了大量的修改,以更好地處理硬體內部特性和向量化,比如[dotnet/runtime#35421](https://github.com/dotnet/runtime/pull/35421), [dotnet/runtime#31834](https://github.com/dotnet/runtime/pull/31834), [dotnet/runtime#1280](https://github.com/dotnet/runtime/pull/1280), [dotnet/runtime#35857](https://github.com/dotnet/runtime/pull/35857), [dotnet/runtime#36267](https://github.com/dotnet/runtime/pull/36267)和 [dotnet/runtime#35525](https://github.com/dotnet/runtime/pull/35525)。
## Runtime helpers
GC和JIT代表了執行時的大部分,但是在執行時中這些元件之外仍然有相當一部分功能,並且這些功能也有類似的改進。
有趣的是,JIT不會為所有東西從頭生成程式碼。JIT在很多地方呼叫了預先存在的 helpers函式,執行時提供這些 helpers,對這些 helpers的改進可以對程式產生有意義的影響。[dotnet/runtime#23548](https://github.com/dotnet/coreclr/pull/23548) 是一個很好的例子。在像System這樣的圖書館中。Linq,我們避免為協變介面新增額外的型別檢查,因為它們的開銷比普通介面高得多。本質上,[dotnet/runtime#23548](https://github.com/dotnet/coreclr/pull/23548) (隨後在[dotnet/runtime#34427](https://github.com/dotnet/runtime/pull/34427)中進行了調整)增加了一個快取,這樣這些資料轉換的代價被平攤,最終總體上更快了。這從一個簡單的微基準測試中就可以明顯看出:
```csharp private List _list = new List(); // IReadOnlyCollection is covariant [Benchmark] public bool IsIReadOnlyCollection() => IsIReadOnlyCollection(_list); [MethodImpl(MethodImplOptions.NoInlining)] private static bool IsIReadOnlyCollection(object o) => o is IReadOnlyCollection; ``` | Method | Runtime | Mean | Ratio | Code Size | | --- | --- | --- | --- | --- | | IsIReadOnlyCollection | .NET FW 4.8 | 105.460 ns | 1.00 | 53 B | | IsIReadOnlyCollection | .NET Core 3.1 | 56.252 ns | 0.53 | 59 B | | IsIReadOnlyCollection | .NET 5.0 | 3.383 ns | 0.03 | 45 B |
另一組有影響的更改出現在[dotnet/runtime#32270](https://github.com/dotnet/runtime/pull/32270)中(在[dotnet/runtime#31957](https://github.com/dotnet/runtime/pull/31957)中支援JIT)。在過去,泛型方法只維護了幾個專用的字典槽,可以用於快速查詢與泛型方法相關的型別;一旦這些槽用完,它就會回到一個較慢的查詢表。這種限制不再存在,這些更改使快速查詢槽可用於所有通用查詢。
```csharp [Benchmark] public void GenericDictionaries() { for (int i = 0; i < 14; i++) GenericMethod(i); } [MethodImpl(MethodImplOptions.NoInlining)] private static object GenericMethod(int level) { switch (level) { case 0: return typeof(T); case 1: return typeof(List); case 2: return typeof(List>); case 3: return typeof(List>>); case 4: return typeof(List>>>); case 5: return typeof(List>>>>); case 6: return typeof(List>>>>>); case 7: return typeof(List>>>>>>); case 8: return typeof(List>>>>>>>); case 9: return typeof(List>>>>>>>>); case 10: return typeof(List>>>>>>>>>); case 11: return typeof(List>>>>>>>>>>); case 12: return typeof(List>>>>>>>>>>>); default: return typeof(List>>>>>>>>>>>>); } } ``` | Method | Runtime | Mean | Ratio | | --- | --- | --- | --- | | GenericDictionaries | .NET FW 4.8 | 104.33 ns | 1.00 | | GenericDictionaries | .NET Core 3.1 | 76.71 ns | 0.74 | | GenericDictionaries | .NET 5.0 | 51.53 ns | 0.49 |
## Text Processing
基於文字的處理是許多應用程式的基礎,並且在每個版本中都花費了大量的精力來改進基礎構建塊,其他所有內容都構建在這些基礎構建塊之上。這些變化從 helpers處理單個字元的微優化一直延伸到整個文字處理庫的大修。
系統。Char在NET 5中得到了一些不錯的改進。例如,[dotnet/coreclr#26848](https://github.com/dotnet/coreclr/pull/26848)提高了char的效能。通過調整實現來要求更少的指令和更少的分支。改善char。IsWhiteSpace隨後在一系列依賴於它的其他方法中出現,比如string.IsEmptyOrWhiteSpace和調整:
```csharp [Benchmark] public int Trim() => " test ".AsSpan().Trim().Length; ``` | Method | Runtime | Mean | Ratio | Code Size | | --- | --- | --- | --- | --- | | Trim | .NET FW 4.8 | 21.694 ns | 1.00 | 569 B | | Trim | .NET Core 3.1 | 8.079 ns | 0.37 | 377 B | | Trim | .NET 5.0 | 6.556 ns | 0.30 | 365 B |
另一個很好的例子,[dotnet/runtime#35194](https://github.com/dotnet/runtime/pull/35194)改進了char的效能。ToUpperInvariant和char。通過改進各種方法的內聯性,將呼叫路徑從公共api簡化到核心功能,並進一步調整實現以確保JIT生成最佳程式碼,從而實現owerinvariant。
```csharp [Benchmark] [Arguments("It's exciting to see great performance!")] public int ToUpperInvariant(string s) { int sum = 0; for (int i = 0; i < s.Length; i++) sum += char.ToUpperInvariant(s[i]); return sum; } ``` | Method | Runtime | Mean | Ratio | Code Size | | --- | --- | --- | --- | --- | | ToUpperInvariant | .NET FW 4.8 | 208.34 ns | 1.00 | 171 B | | ToUpperInvariant | .NET Core 3.1 | 166.10 ns | 0.80 | 164 B | | ToUpperInvariant | .NET 5.0 | 69.15 ns | 0.33 | 105 B |
除了單個字元之外,實際上在.NET Core的每個版本中,我們都在努力提高現有格式化api的速度。這次釋出也沒有什麼不同。儘管之前的版本取得了巨大的成功,但這一版本將門檻進一步提高。
`Int32.ToString()` 是一個非常常見的操作,重要的是它要快。來自[@ts2do](https://github.com/ts2do)的[dotnet/runtime#32528](https://github.com/dotnet/runtime/pull/32528) 通過為該方法使用的關鍵格式化例程新增不可連結的快速路徑,並通過簡化各種公共api到達這些例程的路徑,使其更快。其他原始ToString操作也得到了改進。例如,[dotnet/runtime#27056](https://github.com/dotnet/coreclr/pull/27056)簡化了一些程式碼路徑,以減少從公共API到實際將位寫入記憶體的位置的冗餘。
```csharp [Benchmark] public string ToString12345() => 12345.ToString(); [Benchmark] public string ToString123() => ((byte)123).ToString(); ```
| Method | Runtime | Mean | Ratio | Allocated | | --- | --- | --- | --- | --- | | ToString12345 | .NET FW 4.8 | 45.737 ns | 1.00 | 40 B | | ToString12345 | .NET Core 3.1 | 20.006 ns | 0.44 | 32 B | | ToString12345 | .NET 5.0 | 10.742 ns | 0.23 | 32 B | | | | | | | | ToString123 | .NET FW 4.8 | 42.791 ns | 1.00 | 32 B | | ToString123 | .NET Core 3.1 | 18.014 ns | 0.42 | 32 B | | ToString123 | .NET 5.0 | 7.801 ns | 0.18 | 32 B |
類似的,在之前的版本中,我們對DateTime和DateTimeOffset做了大量的優化,但這些改進主要集中在日/月/年/等等的轉換速度上。將資料轉換為正確的字元或位元組,並將其寫入目的地。在[dotnet/runtime#1944](https://github.com/dotnet/runtime/pull/1944)中,[@ts2do](https://github.com/ts2do)專注於之前的步驟,優化提取日/月/年/等等。DateTime{Offset}從原始滴答計數中儲存。最終非常富有成果,導致能夠輸出格式如“o”(“往返日期/時間模式”)比以前快了30%(變化也應用同樣的分解優化在其他地方在這些元件的程式碼庫需要從一個DateTime,但改進是最容易顯示在一個標準格式):
```csharp private byte[] _bytes = new byte[100]; private char[] _chars = new char[100]; private DateTime _dt = DateTime.Now; [Benchmark] public bool FormatChars() => _dt.TryFormat(_chars, out _, "o"); [Benchmark] public bool FormatBytes() => Utf8Formatter.TryFormat(_dt, _bytes, out _, 'O'); ``` | Method | Runtime | Mean | Ratio | | --- | --- | --- | --- | | FormatChars | .NET Core 3.1 | 242.4 ns | 1.00 | | FormatChars | .NET 5.0 | 176.4 ns | 0.73 | | | | | | | FormatBytes | .NET Core 3.1 | 235.6 ns | 1.00 | | FormatBytes | .NET 5.0 | 176.1 ns | 0.75 |
對字串的操作也有很多改進,比如[dotnet/coreclr#26621](https://github.com/dotnet/coreclr/pull/26621)和[dotnet/coreclr#26962](https://github.com/dotnet/coreclr/pull/26962),在某些情況下顯著提高了區域性感知的Linux上的起始和結束操作的效能。
當然,低階處理是很好的,但是現在的應用程式花費了大量的時間來執行高階操作,比如以特定格式編碼資料,比如之前的.NET Core版本是對Encoding.UTF8進行了優化,但在.NET 5中仍有進一步的改進。[dotnet/runtime#27268](https://github.com/dotnet/coreclr/pull/27268)優化它,特別是對於較小的投入,以更好地利用堆疊分配和改進了JIT devirtualization (JIT是能夠避免虛擬排程由於能夠發現實際的具體型別例項的處理)。
```csharp [Benchmark] public string Roundtrip() { byte[] bytes = Encoding.UTF8.GetBytes("this is a test"); return Encoding.UTF8.GetString(bytes); } ``` | Method | Runtime | Mean | Ratio | Allocated | | --- | --- | --- | --- | --- | | Roundtrip | .NET FW 4.8 | 113.69 ns | 1.00 | 96 B | | Roundtrip | .NET Core 3.1 | 49.76 ns | 0.44 | 96 B | | Roundtrip | .NET 5.0 | 36.70 ns | 0.32 | 96 B |
與UTF8同樣重要的是“ISO-8859-1”編碼,也被稱為“Latin1”(現在公開表示為編碼)。Encoding.Latin1通過[dotnet/runtime#37550](https://github.com/dotnet/runtime/pull/37550)),也非常重要,特別是對於像HTTP這樣的網路協議。[dotnet/runtime#32994](https://github.com/dotnet/runtime/pull/32994)對其實現進行了向量化,這在很大程度上是基於以前對Encoding.ASCII進行的類似優化。這將產生非常好的效能提升,這可以顯著地影響諸如HttpClient這樣的客戶機和諸如Kestrel這樣的伺服器中的高層使用。
```csharp private static readonly Encoding s_latin1 = Encoding.GetEncoding("iso-8859-1"); [Benchmark] public string Roundtrip() { byte[] bytes = s_latin1.GetBytes("this is a test. this is only a test. did it work?"); return s_latin1.GetString(bytes); } ``` | Method | Runtime | Mean | Allocated | | --- | --- | --- | --- | | Roundtrip | .NET FW 4.8 | 221.85 ns | 209 B | | Roundtrip | .NET Core 3.1 | 193.20 ns | 200 B | | Roundtrip | .NET 5.0 | 41.76 ns | 200 B |
編碼效能的改進也擴充套件到了System.Text.Encodings中的編碼器。來自[@gfoidl](https://github.com/gfoidl)的PRs [dotnet/corefx#42073](https://github.com/dotnet/corefx/pull/42073)和[dotnet/runtime#284](https://github.com/dotnet/runtime/pull/284)改進了各種TextEncoder型別。這包括使用SSSE3指令向量化FindFirstCharacterToEncodeUtf8以及JavaScriptEncoder中的FindFirst