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

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

在.NET Core以前的版本中,其實已經在博客中介紹了在該版本中發現的重大性能改進。 從.NET Core 2.0到.NET Core 2.1到.NET Core 3.0的每一篇文章,發現
談論愈來愈多的東西。 然而有趣的是,每次都想知道下一次是否有足夠的意義的改進以保證再發表一篇文章。 .NET 5已經實現了許多性能改進,儘管直到今年秋天才計劃發佈最終版本,而且到那時頗有可能會有更多的改進,可是還要強調一下,如今已提供的改進。 在這篇文章中,重點介紹約250個PR,這些請求爲整個.NET 5的性能提高作出了巨大貢獻。
html

安裝


Benchmark.NET如今是衡量.NET代碼性能的規範工具,可輕鬆分析代碼段的吞吐量和分配。 所以,本文中大部分示例都是使用使用該工具編寫的微基準來衡量的。首先建立了一個目錄,而後使用dotnet工具對其進行了擴展:
c++

mkdir Benchmarks
cd Benchmarks
dotnet new console


生成的Benchmarks.csproj的內容擴展爲以下所示:
git

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
    <ServerGarbageCollection>true</ServerGarbageCollection>
    <TargetFrameworks>net5.0;netcoreapp3.1;net48</TargetFrameworks>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="benchmarkdotnet" Version="0.12.1" />
  </ItemGroup>

  <ItemGroup Condition=" '$(TargetFramework)' == 'net48' ">
    <PackageReference Include="System.Memory" Version="4.5.4" />
    <PackageReference Include="System.Text.Json" Version="4.7.2" />
    <Reference Include="System.Net.Http" />
  </ItemGroup>

</Project>


這樣,就能夠針對.NET Framework 4.8,.NET Core 3.1和.NET 5執行基準測試(目前已爲Preview 8安裝了每晚生成的版本)。.csproj還引用Benchmark.NET NuGet軟件包(其最新版本爲12.1版),以便可以使用其功能,而後引用其餘幾個庫和軟件包,特別是爲了支持可以在其上運行測試 .NET Framework 4.8。
而後,將生成的Program.cs文件更新到同一文件夾中,以下所示:
github

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"的位置。
爲了運行基準測試,而後作:
web

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」。 所以,當指的是「runtime」時,指的是coreclr,即便從.NET 5開始,它也包含多個運行時,而且全部這些都已獲得改進。
  • 大多數示例都在Windows上運行,由於也但願可以與.NET Framework 4.8進行比較。 可是,除非另有說明,不然全部顯示的示例均適用於Windows,Linux和macOS。
  • 須要注意的是: 這裏的全部測量數據都是在的臺式機上進行的,測量結果可能會有所不一樣。微基準測試對許多因素都很是敏感,包括處理器數量、處理器架構、內存和緩存速度等等。可是,通常來講,我關注的是性能改進,幷包含了一般可以承受此類差別的示例。


讓咱們開始吧…


express

GC


對於全部對.NET和性能感興趣的人來講,垃圾收集一般是他們最關心的。在減小分配上花費了大量的精力,不是由於分配行爲自己特別昂貴,而是由於經過垃圾收集器(GC)清理這些分配以後的後續成本。然而,不管減小分配須要作多少工做,絕大多數工做負載都會致使這種狀況發生,所以,重要的是要不斷提升GC可以完成的任務和速度。


這個版本在改進GC方面作了不少工做。例如, dotnet/coreclr#25986 爲GC的「mark」階段實現了一種形式的工做竊取。.NET GC是一個「tracing」收集器,這意味着(在很是高的級別上)當它運行時,它從一組「roots」(已知的固有可訪問的位置,好比靜態字段)開始,從一個對象遍歷到另外一個對象,將每一個對象「mark」爲可訪問;在全部這些遍歷以後,任何沒有標記的對象都是不可訪問的,能夠收集。此標記表明了執行集合所花費的大部分時間,而且此PR經過更好地平衡集合中涉及的每一個線程執行的工做來改進標記性能。當使用「Server GC」運行時,每一個核都有一個線程參與收集,當線程完成分配給它們的標記工做時,它們如今可以從其餘線程「steal」 未完成的工做,以幫助更快地完成整個收集。


另外一個例子是,dotnet/runtime#35896 「ephemeral」段的解壓進行了優化(gen0和gen1被稱爲 「ephemeral」,由於它們是預期只持續很短期的對象)。在段的最後一個活動對象以後,將內存頁返回給操做系統。那麼GC的問題就變成了,這種解解應該在何時發生,以及在任什麼時候候應該解解多少,由於在不久的未來,它可能須要爲額外的分配分配額外的頁面。


或者以dotnet/runtime#32795,爲例,它經過減小在GC靜態掃描中涉及的鎖爭用,提升了在具備較高核心計數的機器上的GC可伸縮性。或者dotnet/runtime#37894,它避免了代價高昂的內存重置(本質上是告訴操做系統相關的內存再也不感興趣),除非GC看到它處於低內存的狀況。或者dotnet/runtime#37159,它(雖然尚未合併,預計將用於.NET5 )構建在@damageboy的工做之上,用於向量化GC中使用的排序。或者 dotnet/coreclr#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,它將原始類型數組排序的實現從coreclr的本地代碼移到了Corelib的c#中。除了這些代碼以外,它還爲新的公共api提供了對跨度進行排序的支持,它還下降了對較小數組進行排序的成本,由於排序的成本主要來自於從託管代碼的轉換。咱們能夠在一個小的基準測試中看到這一點,它只是使用數組。對包含10個元素的int[], double[]和string[]數組進行排序:
json

public class DoubleSorting : Sorting<double> { protected override double GetNext() => _random.Next(); }
public class Int32Sorting : Sorting<int> { protected override int GetNext() => _random.Next(); }
public class StringSorting : Sorting<string>
{
    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<T>
{
    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 添加了System.Half,一個新的原始16位浮點,而且在託管代碼中,這個排序實現的優化幾乎當即應用到它,而之前的本地實現須要大量的額外工做,由於沒有c++標準類型的一半。可是,這裏還有一個更有影響的性能優點,這讓咱們回到我開始討論的地方:GC。


GC的一個有趣指標是「pause time」,這實際上意味着GC必須暫停運行時多長時間才能執行其工做。更長的暫停時間對延遲有直接的影響,而延遲是全部工做負載方式的關鍵指標。正如前面提到的,GC可能須要暫停線程爲了獲得一個一致的世界觀,並確保它能安全地移動對象,可是若是一個線程正在執行C/c++代碼在運行時,GC可能須要等到調用完成以前暫停的線程。所以,咱們在託管代碼而不是本機代碼中作的工做越多,GC暫停時間就越好。咱們能夠使用相同的數組。排序的例子,看看這個。考慮一下這個程序:
小程序

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上運行時,我獲得的秒數是這樣的

6.6419048
5.5663149
5.7430339
6.032052
7.8892468


在這裏,GC很難中斷執行排序的線程,致使GC暫停時間遠遠高於預期。幸運的是,當我在 .NET5 上運行這個時,我獲得了這樣的數字:

0.159311
0.159453
0.1594669
0.1593328
0.1586566


這正是咱們預測的結果。經過移動數組。將實現排序到託管代碼中,這樣運行時就能夠在須要時更容易地掛起實現,咱們使GC可以更好地完成其工做。


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


關於這組更改的最後一個想法是,須要注意的另外一件有趣的事情是,在一個版本中所作的微優化是如何基於後來被證實無效的假設的,而且當使用這種微優化時,須要準備並願意適應。在個人.NET Core 3.0博客中,我提到了像dotnet/coreclr#21756這樣的「peanut butter」式的改變,它改變了不少使用數組的調用站點。複製(源,目標,長度),而不是使用數組。複製(source, sourceOffset, destination, destinationOffset, length),由於前者獲取源數組和目標數組的下限的開銷是可測量的。可是經過前面提到的將數組處理代碼移動到c#的一系列更改,更簡單的重載的開銷消失了,使其成爲這些操做更簡單、更快的選擇。這樣,.NET5 PRs dotnet/coreclr#27641dotnet/corefx#42343切換了全部這些呼叫站點,更多地回到使用更簡單的過載。dotnet/runtime#36304是另外一個取消以前優化的例子,由於更改使它們過期或實際上有害。你老是可以傳遞一個字符到字符串。分裂,如version.Split (' . ')。然而,問題是,這個綁定到Split的惟一重載是Split(params char[] separator),這意味着每次這樣的調用都會致使c#編譯器生成一個char[]分配。爲了解決這個問題,之前的版本添加了緩存,提早分配數組並將它們存儲到靜態中,而後能夠被分割調用使用,以免每一個調用都使用char[]。既然.NET中有一個Split(char separator, StringSplitOptions options = StringSplitOptions. none)重載,咱們就再也不須要數組了。
做爲最後一個示例,我展現了將代碼移出運行時並轉移到託管代碼中如何幫助GC暫停,可是固然還有其餘方式能夠使運行時中剩餘的代碼對此有所幫助。dotnet/runtime#36179經過確保運行時處於代碼爭搶模式下(例如獲取「Watson」存儲桶參數(基本上是一組用於惟一標識此特定異常和調用堆棧以用於報告目的的數據)),從而減小了因爲異常處理而致使的GC暫停。 。暫停。

JIT


.NET5 也是即時(JIT)編譯器的一個使人興奮的版本,該版本中包含了各類各樣的改進。與任何編譯器同樣,對JIT的改進能夠產生普遍的影響。一般,單獨的更改對單獨的代碼段的影響很小,可是這樣的更改會被它們應用的地方的數量放大。
能夠向JIT添加的優化的數量幾乎是無限的,若是給JIT無限的時間來運行這種優化,JIT就能夠爲任何給定的場景建立最優代碼。可是JIT的時間並非無限的。JIT的「即時」特性意味着它在應用程序運行時執行編譯:當調用還沒有編譯的方法時,JIT須要按需爲其提供彙編代碼。這意味着在編譯完成以前線程不能向前推動,這反過來意味着JIT須要在應用什麼優化以及如何選擇使用有限的時間預算方面有策略。各類技術用於給JIT更多的時間,好比使用「提早」(AOT)編譯應用程序的一些部分作儘量多的編譯工做前儘量執行應用程序(例如,AOT編譯核心庫都使用一個叫「ReadyToRun」的技術,你可能會聽到稱爲「R2R」甚至「crossgen」,是產生這些圖像的工具),或使用「tiered compilation」,它容許JIT在最初編譯一個應用了從少到少優化的方法,所以速度很是快,只有在它被認爲有價值的時候(即該方法被重複使用的時候),纔會花更多的時間使用更多優化來從新編譯它。然而,更廣泛的狀況是,參與JIT的開發人員只是選擇使用分配的時間預算進行優化,根據開發人員編寫的代碼和他們使用的代碼模式,這些優化被證實是有價值的。這意味着,隨着.NET的發展並得到新的功能、新的語言特性和新的庫特性,JIT也會隨着適合於編寫的較新的代碼風格的優化而發展。
一個很好的例子是@benaadamsdotnet/runtime#32538。 Span 一直滲透到.NET堆棧的全部層,由於從事運行時,核心庫,ASP.NET Core的開發人員以及其餘人在編寫安全有效的代碼(也統一了字符串處理)時認識到了它的強大功能 ,託管數組,本機分配的內存和其餘形式的數據。 相似地,值類型(結構)被愈來愈廣泛地用做經過堆棧分配避免對象分配開銷的一種方式。 可是,對此類類型的嚴重依賴也給運行時帶來了更多麻煩。 coreclr運行時使用「precise」 garbage collector,這意味着GC可以100%準確地跟蹤哪些值引用託管對象,哪些值不引用託管對象; 這樣作有好處,但也有代價(相反,mono運行時使用「conservative」垃圾收集器,這具備一些性能上的好處,但也意味着它能夠解釋堆棧上的任意值,而該值剛好與 被管理對象的地址做爲對該對象的實時引用)。 這樣的代價之一是,JIT須要經過確保在GC注意以前將任何能夠解釋爲對象引用的局部都清零來幫助GC。 不然,GC可能最終會在還沒有設置的本地中看到一個垃圾值,並假定它引用的是有效對象,這時可能會發生「bad things」。 參考當地人越多,須要進行的清理越多。 若是您只清理一些當地人,那可能不會引發注意。 可是隨着數量的增長,清除這些本地對象所花費的時間可能加起來,尤爲是在很是熱的代碼路徑中使用的一種小方法中。 這種狀況在跨度和結構中變得更加廣泛,在這種狀況下,編碼模式一般會致使須要爲零的更多引用(Span 包含引用)。 前面提到的PR經過更新JIT生成的序號塊的代碼來解決此問題,這些序號塊使用xmm寄存器而不是rep stosd指令來執行該清零操做。 有效地,它對歸零進行矢量化處理。 您能夠經過如下基準測試看到此影響:

[Benchmark]
public int Zeroing()
{
    ReadOnlySpan<char> s1 = "hello world";
    ReadOnlySpan<char> s2 = Nop(s1);
    ReadOnlySpan<char> s3 = Nop(s2);
    ReadOnlySpan<char> s4 = Nop(s3);
    ReadOnlySpan<char> s5 = Nop(s4);
    ReadOnlySpan<char> s6 = Nop(s5);
    ReadOnlySpan<char> s7 = Nop(s6);
    ReadOnlySpan<char> s8 = Nop(s7);
    ReadOnlySpan<char> s9 = Nop(s8);
    ReadOnlySpan<char> 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<char> Nop(ReadOnlySpan<char> 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#規範要求在執行開發人員的代碼以前,將全部本地變量初始化爲默認值。你能夠經過這樣一個例子來了解這一點:

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):

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。若是咱們對前面的示例稍加修改,就能夠將屬性添加到整個模塊中:

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中,.NET5 中的核心庫如今都使用這個屬性來禁用.locals init(在之前的版本中,.locals init在構建核心庫時經過編譯後的一個步驟刪除)。請注意,c#編譯器只容許在不安全的上下文中使用SkipLocalsInit,由於它很容易致使未通過適當驗證的代碼損壞(所以,若是/當您應用它時,請三思)。


除了使零的速度更快,也有改變,以消除零徹底。例如,dotnet/runtime#31960, dotnet/runtime#36918, dotnet/runtime#37786,和dotnet/runtime#38314 都有助於消除零,當JIT能夠證實它是重複的。
這樣的零是託管代碼的一個例子,運行時須要它來保證其模型和上面語言的需求。另外一種此類稅收是邊界檢查。使用託管代碼的最大優點之一是,在默認狀況下,整個類的潛在安全漏洞都變得可有可無。運行時確保數組、字符串和span的索引被檢查,這意味着運行時注入檢查以確保被請求的索引在被索引的數據的範圍內(即greather大於或等於0,小於數據的長度)。這裏有一個簡單的例子:

public static char Get(string s, int i) => s[i];


爲了保證這段代碼的安全,運行時須要生成一個檢查,檢查i是否在字符串s的範圍內,這是JIT經過以下程序集完成的:

; 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已經可以在各類狀況下刪除邊界檢查。例如,當你寫循環:

int[] arr = ...;
for (int i = 0; i < arr.Length; i++)
    Use(arr[i]);


JIT能夠證實我永遠不會超出數組的邊界,所以它能夠省略它將生成的邊界檢查。在.NET5 中,它能夠在更多的地方刪除邊界檢查。例如,考慮這個函數,它將一個整數的字節做爲字符寫入一個span:

private static bool TryToHex(int value, Span<char> span)
{
    if ((uint)span.Length <= 7)
        return false;

    ReadOnlySpan<byte> 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#編譯器的優化。注意:

ReadOnlySpan<byte> 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時都要分配一個字節數組。事實上,它並非這樣的,它實際上比咱們作的更好:

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<byte> map = s_map;


C#編譯器能夠識別直接分配給ReadOnlySpan的新字節數組的模式(它也能夠識別sbyte和bool,但因爲字節關係,沒有比字節大的)。由於數組的性質被span徹底隱藏了,C#編譯器經過將字節實際存儲到程序集的數據部分而發出這些字節,而span只是經過將靜態數據和長度的指針包裝起來而建立的:

IL_000c: ldsflda valuetype '<PrivateImplementationDetails>'/'__StaticArrayInitTypeSize=16' '<PrivateImplementationDetails>'::'2125B2C332B1113AAE9BFC5E9F7E3B4C91D828CB942C2DF1EEB02502ECCAE9E9'
IL_0011: ldc.i4.s 16
IL_0013: newobj instance void valuetype [System.Runtime]System.ReadOnlySpan'1<uint8>::.ctor(void*, int32)


因爲ldc.i4,這對於本次JIT討論很重要。s16在上面。這就是IL加載16的長度來建立跨度,JIT能夠看到這一點。它知道跨度的長度是16,這意味着若是它能夠證實訪問老是大於或等於0且小於16的值,它就不須要對訪問進行邊界檢查。dotnet/runtime#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中的@nathan-moore。我提到過,JIT已經可以刪除很是常見的從0迭代到數組、字符串或span長度的模式的邊界檢查,可是在此基礎上還有一些比較常見的變化,但之前沒有認識到。例如,考慮這個微基準測試,它調用一個方法來檢測一段整數是否被排序:

private int[] _array = Enumerable.Range(0, 1000).ToArray();

[Benchmark]
public bool IsSorted() => IsSorted(_array);

private static bool IsSorted(ReadOnlySpan<int> 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異常(這裏))。但有時指令只用於null檢查,而不是完成其餘必要的功能,並且只要須要的null檢查是因爲某些指令發生的,沒必要要的重複指令能夠被刪除。考慮這段代碼:

private (int i, int j) _value;

[Benchmark]
public int NullCheck() => _value.j++;


做爲一個可運行的基準測試,它所作的工做太少,沒法用基準測試進行準確的度量.NET,但這是查看生成的彙編代碼的好方法。在.NET Core 3.1中,此方法產生以下assembly:

; 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#1735dotnet/runtime#32641這樣的PRs,這樣的重複被JIT比之前更多地識別,對於.NET 5,咱們如今獲得了:

; Program.NullCheck()
       add       rcx,0C
       mov       eax,[rcx]
       lea       edx,[rax+1]
       mov       [rcx],edx
       ret
; Total bytes of code 12

協方差是JIT須要注入檢查以確保開發人員不會意外地破壞類型或內存安全性的另外一種狀況。考慮一下代碼

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, JIT如今可以消除更多的協方差檢查,特別是在數組的元素類型是密封的狀況下,好比string。所以,像這樣的微基準如今運行得更快了:

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,例如:

using System;

class Program
{
    static void Main() => new Span<A>(new B[42]);
}

class A { }
class B : A { }

將致使異常:

System.ArrayTypeMismatchException: Attempted to access an element as a type incompatible with the array


該異常源於對Span 的構造函數的檢查

if (!typeof(T).IsValueType && array.GetType() != typeof(T[]))
    ThrowHelper.ThrowArrayTypeMismatchException();

PR dotnet/runtime#32790就是這樣優化數組的.GetType()!= typeof(T [])檢查什麼時候密封T,而dotnet/runtime#1157識別typeof(T).IsValueType模式並將其替換爲常量 值(PR dotnet/runtime#1195對於typeof(T1).IsAssignableFrom(typeof(T2))進行了相同的操做)。 這樣作的最終結果是極大地改善了微基準,例如:

class A { }
sealed class B : A { }

private B[] _array = new B[42];

[Benchmark]
public int Ctor() => new Span<B>(_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上生成的內容:

; 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的內容:

; 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。當咱們第一次將本機數組排序實現移動到managed時,咱們無心中致使了浮點值的迴歸,這個迴歸被@nietras 發現,隨後在dotnet/runtime#37941中修復。迴歸是因爲本機實現使用一個特殊的優化,咱們失蹤的管理端口(浮點數組,將全部NaN值數組的開始,後續的比較操做能夠忽略NaN)的可能性,咱們成功了。然而,問題是這個的方式表達並無致使大量的代碼重複:本機實現模板,使用和管理實現使用泛型,但限制與泛型等,內聯 helpers介紹,以免大量的代碼重複致使non-inlineable在每一個比較採用那種方法調用。PR dotnet/runtime#38229經過容許JIT在同一類型內嵌共享泛型代碼解決了這個問題。考慮一下這個微基準測試:

private C c1 = new C() { Value = 1 }, c2 = new C() { Value = 2 }, c3 = new C() { Value = 3 };

[Benchmark]
public int Compare() => Comparer<C>.Smallest(c1, c2, c3);

class Comparer<T> where T : IComparable<T>
{
    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<C>
{
    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工做的人們實際上很是關注代碼大小,在許多狀況下,將其做爲判斷更改是否有益的主要指標。更小的代碼並不老是更快的代碼(能夠是相同大小的指令,但開銷不一樣),但從高層次上來講,這是一個合理的度量,更小的代碼確實有直接的好處,好比對指令緩存的影響更小,須要加載的代碼更少,等等。在某些狀況下,更改徹底集中在減小代碼大小上,好比在出現沒必要要的重複的狀況下。考慮一下這個簡單的基準:

private int _offset = 0;

[Benchmark]
public int Throw helpers()
{
    var arr = new int[10];
    var s0 = new Span<int>(arr, _offset, 1);
    var s1 = new Span<int>(arr, _offset + 1, 1);
    var s2 = new Span<int>(arr, _offset + 2, 1);
    var s3 = new Span<int>(arr, _offset + 3, 1);
    var s4 = new Span<int>(arr, _offset + 4, 1);
    var s5 = new Span<int>(arr, _offset + 5, 1);
    return s0[0] + s1[0] + s2[0] + s3[0] + s4[0] + s5[0];
}


Span 構造函數 參數驗證,,T是一個值類型時,結果有兩個叫網站ThrowHelper類上的方法,一個扔一個失敗的null檢查時拋出的輸入數組和一個偏移量和計數的範圍(像ThrowArgumentNullException ThrowHelper包含non-inlinable方法,其中包含實際的扔,避免了相關代碼大小在每一個調用網站;JIT目前還不能「outlining」(與「inlining」相反),所以須要在重要的狀況下手工完成)。在上面的示例中,咱們建立了6個Span,這意味着對Span構造函數的6次調用,全部這些調用都將內聯。JIT數組爲空,因此它能夠消除零檢查和ThrowArgumentNullException內聯代碼,可是它不知道是否偏移量和計算範圍內,所以它須要保留ThrowHelper範圍檢查和調用站點。ThrowArgumentOutOfRangeException方法。在.NET Core 3.1中,這個Throw helpers方法生成了以下代碼:

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, JIT可以識別這種重複,而不是全部的6個呼叫站點,它將最終合併成一個:

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致使JIT將數組的長度視爲無符號,這使得JIT可以對在長度上執行的某些數學運算(例如除法)使用更好的指令。 dotnet/runtime#25458 使JIT能夠對某些無符號整數運算使用更快的基於0的比較。 當開發人員實際編寫> = 1時,使用等於!= 0的值。dotnet/runtime#1378容許JIT將「 constantString」 .Length識別爲常量值。 dotnet/runtime#26740 經過刪除nop填充來減少ReadyToRun圖像的大小。 dotnet/runtime#330234使用加法而不是乘法來優化當x爲浮點數或雙精度數時執行x * 2時生成的指令。dotnet/runtime#27060改進了爲Math.FusedMultiplyAdd內部函數生成的代碼。 dotnet/runtime#27384經過使用比之前更好的籬笆指令使ARM64上的易失性操做便宜,而且dotnet/runtime#38179在ARM64上執行窺視孔優化以刪除大量冗餘mov指令。 等等。


JIT中還有一些默認禁用的重要更改,目的是得到關於它們的真實反饋,並可以在默認狀況下post-啓用它們。淨5。例如,dotnet/runtime#32969提供了「On Stack Replacement」(OSR)的初始實現。我在前面提到了分層編譯,它使JIT可以首先爲一個方法生成優化最少的代碼,而後當該方法被證實是重要的時,用更多的優化從新編譯該方法。這容許代碼運行得更快,而且只有在運行時才升級有效的方法,從而實現更快的啓動時間。可是,分層編譯依賴於替換實現的能力,下次調用它時,將調用新的實現。可是長時間運行的方法呢?對於包含循環(或者,更具體地說,向後分支)的方法,分層編譯在默認狀況下是禁用的,由於它們可能會運行很長時間,以致於沒法及時使用替換。OSR容許方法在執行代碼時被更新,而它們是「在堆棧上」的;PR中包含的設計文檔中有不少細節(也與分層編譯有關,dotnet/runtime#1457改進了調用計數機制,分層編譯經過這種機制決定哪些方法應該從新編譯以及什麼時候從新編譯)。您能夠經過將COMPlus_TC_QuickJitForLoops和COMPlus_TC_OnStackReplacement環境變量設置爲1來試驗OSR。另外一個例子是,dotnet/runtime#1180 改進了try塊內代碼的生成代碼質量,使JIT可以在寄存器中保存之前不能保存的值。您能夠經過將COMPlus_EnableEHWriteThr環境變量設置爲1來進行試驗。


還有一堆等待拉請求JIT還沒有合併,但極可能在.NET 5發佈(除此以外,我預計還有更多在.NET 5發佈以前尚未發佈的內容)。例如,dotnet/runtime#32716容許JIT替換一些分支比較,如a == 42 ?3: 2無分支實現,當硬件沒法正確預測將採用哪一個分支時,能夠幫助提升性能。或dotnet/runtime#37226,它容許JIT採用像「hello」[0]這樣的模式並將其替換爲h;雖然開發人員一般不編寫這樣的代碼,但在涉及內聯時,這能夠提供幫助,經過將常量字符串傳遞給內聯的方法,並將其索引到常量位置(一般在長度檢查以後,因爲dotnet/runtime#1378,長度檢查也能夠成爲常量)。或dotnet/runtime#1224,它改進了Bmi2的代碼生成。MultiplyNoFlags內在。或者dotnet/runtime#37836,它將轉換位操做。將PopCount轉換爲一個內因,使JIT可以識別什麼時候使用常量參數調用它,並將整個操做替換爲一個預先計算的常量。或dotnet/runtime#37254,它刪除使用const字符串時發出的空檢查。或者來自@damageboydotnet/runtime#32000 ,它優化了雙重否認。

Intrinsics


在.NET Core 3.0中,超過1000種新的硬件內置方法被添加並被JIT識別,從而使c#代碼可以直接針對指令集,如SSE4和AVX2(docs)。而後,在覈心庫中的一組api中使用了這些工具。可是,intrinsic僅限於x86/x64架構。在.NET 5中,咱們投入了大量的精力來增長數千個組件,特別是針對ARM64,這要感謝衆多貢獻者,特別是來自Arm Holdings的@TamarChristinaArm。與對應的x86/x64同樣,這些內含物在覈心庫功能中獲得了很好的利用。例如,BitOperations.PopCount()方法以前被優化爲使用x86 POPCNT內在的,對於.NET 5,  dotnet/runtime#35636 加強了它,使它也可以使用ARM VCNT或等價的ARM64 CNT。相似地,dotnet/runtime#34486修改了位操做。LeadingZeroCount, TrailingZeroCount和Log2利用相應的instrincs。在更高的級別上,來自@Gnbrkm41dotnet/runtime#33749加強了位數組中的多個方法,以使用ARM64內含物來配合以前添加的對SSE2和AVX2的支持。爲了確保Vector api在ARM64上也能很好地執行,咱們作了不少工做,好比dotnet/runtime#33749dotnet/runtime#36156


除ARM64以外,還進行了其餘工做以向量化更多操做。 例如,@Gnbrkm41還提交了dotnet/runtime#31993,該文件利用x64上的ROUNDPS / ROUNDPD和ARM64上的FRINPT / FRINTM來改進爲新Vector.Ceiling和Vector.Floor方法生成的代碼。 BitOperations(這是一種相對低級的類型,針對大多數操做以最合適的硬件內部函數的1:1包裝器的形式實現),不只在@saucecontroldotnet/runtime#35650中獲得了改進,並且在Corelib中的使用也獲得了改進 更有效率。


最後,JIT進行了大量的修改,以更好地處理硬件內部特性和向量化,好比dotnet/runtime#35421, dotnet/runtime#31834, dotnet/runtime#1280, dotnet/runtime#35857, dotnet/runtime#36267dotnet/runtime#35525

Runtime helpers


GC和JIT表明了運行時的大部分,可是在運行時中這些組件以外仍然有至關一部分功能,而且這些功能也有相似的改進。
有趣的是,JIT不會爲全部東西從頭生成代碼。JIT在不少地方調用了預先存在的 helpers函數,運行時提供這些 helpers,對這些 helpers的改進能夠對程序產生有意義的影響。dotnet/runtime#23548 是一個很好的例子。在像System這樣的圖書館中。Linq,咱們避免爲協變接口添加額外的類型檢查,由於它們的開銷比普通接口高得多。本質上,dotnet/runtime#23548 (隨後在dotnet/runtime#34427中進行了調整)增長了一個緩存,這樣這些數據轉換的代價被平攤,最終整體上更快了。這從一個簡單的微基準測試中就能夠明顯看出:

private List<string> _list = new List<string>();

// IReadOnlyCollection<out T> is covariant
[Benchmark] public bool IsIReadOnlyCollection() => IsIReadOnlyCollection(_list);
[MethodImpl(MethodImplOptions.NoInlining)]  private static bool IsIReadOnlyCollection(object o) => o is IReadOnlyCollection<int>;
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中(在dotnet/runtime#31957中支持JIT)。在過去,泛型方法只維護了幾個專用的字典槽,能夠用於快速查找與泛型方法相關的類型;一旦這些槽用完,它就會回到一個較慢的查找表。這種限制再也不存在,這些更改使快速查找槽可用於全部通用查找。

[Benchmark]
public void GenericDictionaries()
{
    for (int i = 0; i < 14; i++)
        GenericMethod<string>(i);
}

[MethodImpl(MethodImplOptions.NoInlining)]
private static object GenericMethod<T>(int level)
{
    switch (level)
    {
        case 0: return typeof(T);
        case 1: return typeof(List<T>);
        case 2: return typeof(List<List<T>>);
        case 3: return typeof(List<List<List<T>>>);
        case 4: return typeof(List<List<List<List<T>>>>);
        case 5: return typeof(List<List<List<List<List<T>>>>>);
        case 6: return typeof(List<List<List<List<List<List<T>>>>>>);
        case 7: return typeof(List<List<List<List<List<List<List<T>>>>>>>);
        case 8: return typeof(List<List<List<List<List<List<List<List<T>>>>>>>>);
        case 9: return typeof(List<List<List<List<List<List<List<List<List<T>>>>>>>>>);
        case 10: return typeof(List<List<List<List<List<List<List<List<List<List<T>>>>>>>>>>);
        case 11: return typeof(List<List<List<List<List<List<List<List<List<List<List<T>>>>>>>>>>>);
        case 12: return typeof(List<List<List<List<List<List<List<List<List<List<List<List<T>>>>>>>>>>>>);
        default: return typeof(List<List<List<List<List<List<List<List<List<List<List<List<List<T>>>>>>>>>>>>>);
    }
}
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提升了char的性能。經過調整實現來要求更少的指令和更少的分支。改善char。IsWhiteSpace隨後在一系列依賴於它的其餘方法中出現,好比string.IsEmptyOrWhiteSpace和調整:

[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改進了char的性能。ToUpperInvariant和char。經過改進各類方法的內聯性,將調用路徑從公共api簡化到核心功能,並進一步調整實現以確保JIT生成最佳代碼,從而實現owerinvariant。

[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() 是一個很是常見的操做,重要的是它要快。來自@ts2dodotnet/runtime#32528 經過爲該方法使用的關鍵格式化例程添加不可連接的快速路徑,並經過簡化各類公共api到達這些例程的路徑,使其更快。其餘原始ToString操做也獲得了改進。例如,dotnet/runtime#27056簡化了一些代碼路徑,以減小從公共API到實際將位寫入內存的位置的冗餘。

[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中,@ts2do專一於以前的步驟,優化提取日/月/年/等等。DateTime{Offset}從原始滴答計數中存儲。最終很是富有成果,致使可以輸出格式如「o」(「往返日期/時間模式」)比之前快了30%(變化也應用一樣的分解優化在其餘地方在這些組件的代碼庫須要從一個DateTime,但改進是最容易顯示在一個標準格式):

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#26621dotnet/coreclr#26962,在某些狀況下顯著提升了區域性感知的Linux上的起始和結束操做的性能。
固然,低級處理是很好的,可是如今的應用程序花費了大量的時間來執行高級操做,好比以特定格式編碼數據,好比以前的.NET Core版本是對Encoding.UTF8進行了優化,但在.NET 5中仍有進一步的改進。dotnet/runtime#27268優化它,特別是對於較小的投入,以更好地利用堆棧分配和改進了JIT devirtualization (JIT是可以避免虛擬調度因爲可以發現實際的具體類型實例的處理)。

[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),也很是重要,特別是對於像HTTP這樣的網絡協議。dotnet/runtime#32994對其實現進行了向量化,這在很大程度上是基於之前對Encoding.ASCII進行的相似優化。這將產生很是好的性能提高,這能夠顯著地影響諸如HttpClient這樣的客戶機和諸如Kestrel這樣的服務器中的高層使用。

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的PRs dotnet/corefx#42073dotnet/runtime#284改進了各類TextEncoder類型。這包括使用SSSE3指令向量化FindFirstCharacterToEncodeUtf8以及JavaScriptEncoder中的FindFirstCharToEncode。默認實現。

private char[] _dest = new char[1000];

[Benchmark]
public void Encode() => JavaScriptEncoder.Default.Encode("This is a test to see how fast we can encode something that does not actually need encoding", _dest, out _, out _);

Regular Expressions


一種很是特殊但很是常見的解析形式是經過正則表達式。早在4月初,我就分享了一篇關於。net 5中System.Text.RegularExpressions大量性能改進的詳細博客文章。我不打算在這裏重複全部這些內容,可是若是你尚未讀過,我鼓勵你去讀它,由於它表明了圖書館的重大進步。然而,我還在那篇文章中指出,咱們將繼續改進正則表達式,特別是增長了對特殊但常見狀況的更多支持。
其中一個改進是在指定RegexOptions時的換行處理。Multiline,它改變^和$錨點的含義,使其在任何行的開始和結束處匹配,而不只僅是整個輸入字符串的開始和結束處。以前咱們沒有對起始行錨作任何特殊的處理(當Multiline被指定時^),這意味着做爲FindFirstChar操做的一部分(請參閱前面提到的博客文章,瞭解它指的是什麼),咱們不會盡量地跳過它。dotnet/runtime#34566教會FindFirstChar如何使用矢量化的索引向前跳轉到下一個相關位置。這一影響在這個基準中獲得了強調,它處理從Project Gutenberg下載的「羅密歐與朱麗葉」文本:

private readonly string _input = new HttpClient().GetStringAsync("http://www.gutenberg.org/cache/epub/1112/pg1112.txt").Result;
private Regex _regex;

[Params(false, true)]
public bool Compiled { get; set; }

[GlobalSetup]
public void Setup() => _regex = new Regex(@"^.*\blove\b.*$", RegexOptions.Multiline | (Compiled ? RegexOptions.Compiled : RegexOptions.None));

[Benchmark]
public int Count() => _regex.Matches(_input).Count;
Method Runtime Compiled Mean Ratio
Count .NET FW 4.8 False 26.207 ms 1.00
Count .NET Core 3.1 False 21.106 ms 0.80
Count .NET 5.0 False 4.065 ms 0.16
Count .NET FW 4.8 True 16.944 ms 1.00
Count .NET Core 3.1 True 15.287 ms 0.90
Count .NET 5.0 True 2.172 ms 0.13


另外一個改進是在處理RegexOptions.IgnoreCase方面。IgnoreCase的實現使用char.ToLower{Invariant}以得到要比較的相關字符,但因爲區域性特定的映射,這樣作會帶來一些開銷。dotnet/runtime#35185容許在惟一可能與被比較字符小寫的字符是該字符自己時避免這些開銷。

private readonly Regex _regex = new Regex("hello.*world", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private readonly string _input = "abcdHELLO" + new string('a', 128) + "WORLD123";

[Benchmark] public bool IsMatch() => _regex.IsMatch(_input);
Method Runtime Mean Ratio
IsMatch .NET FW 4.8 2,558.1 ns 1.00
IsMatch .NET Core 3.1 789.3 ns 0.31
IsMatch .NET 5.0 129.0 ns 0.05


與此相關的改進是dotnet/runtime#35203,它也服務於RegexOptions。IgnoreCase減小了實現對CultureInfo進行的虛擬調用的數量。緩存TextInfo,而不是CultureInfo從它來。

private readonly Regex _regex = new Regex("Hello, \\w+.", RegexOptions.Compiled | RegexOptions.IgnoreCase);
private readonly string _input = "This is a test to see how well this does.  Hello, world.";

[Benchmark] public bool IsMatch() => _regex.IsMatch(_input);
Method Runtime Mean Ratio
IsMatch .NET FW 4.8 712.9 ns 1.00
IsMatch .NET Core 3.1 343.5 ns 0.48
IsMatch .NET 5.0 100.9 ns 0.14


最近我最喜歡的優化之一是dotnet/runtime#35824(隨後在dotnet/runtime#35936中進一步加強)。regex的認可變化,從一個原子環(一個明確的書面或更常見的一個原子的升級到自動的分析表達式),咱們能夠更新掃描循環中的下一個起始位置(再一次,詳見博客)基於循環的結束,而不是開始。對於許多輸入,這能夠大大減小開銷。使用基準測試和來自https://github.com/mariomka/regex benchmark的數據:

private Regex _email = new Regex(@"[\w\.+-]+@[\w\.-]+\.[\w\.-]+", RegexOptions.Compiled);
private Regex _uri = new Regex(@"[\w]+://[^/\s?#]+[^\s?#]+(?:\?[^\s#]*)?(?:#[^\s]*)?", RegexOptions.Compiled);
private Regex _ip = new Regex(@"(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9])", RegexOptions.Compiled);

private string _input = new HttpClient().GetStringAsync("https://raw.githubusercontent.com/mariomka/regex-benchmark/652d55810691ad88e1c2292a2646d301d3928903/input-text.txt").Result;

[Benchmark] public int Email() => _email.Matches(_input).Count;
[Benchmark] public int Uri() => _uri.Matches(_input).Count;
[Benchmark] public int IP() => _ip.Matches(_input).Count;
Method Runtime Mean Ratio
Email .NET FW 4.8 1,036.729 ms 1.00
Email .NET Core 3.1 930.238 ms 0.90
Email .NET 5.0 50.911 ms 0.05
Uri .NET FW 4.8 870.114 ms 1.00
Uri .NET Core 3.1 759.079 ms 0.87
Uri .NET 5.0 50.022 ms 0.06
IP .NET FW 4.8 75.718 ms 1.00
IP .NET Core 3.1 61.818 ms 0.82
IP .NET 5.0 6.837 ms 0.09


最後,並非全部的焦點都集中在實際執行正則表達式的原始吞吐量上。開發人員使用Regex得到最佳吞吐量的方法之一是指定RegexOptions。編譯,它使用反射發射在運行時生成IL,反過來須要JIT編譯。根據所使用的表達式,Regex可能會輸出大量IL,而後須要大量的JIT處理才能生成彙編代碼。dotnet/runtime#35352改進了JIT自己來幫助解決這種狀況,修復了regex生成的IL觸發的一些可能的二次執行時代碼路徑。而dotnet/runtime#35321對Regex引擎使用的IL操做進行了調整,使其使用的模式更接近於c#編譯器發出的模式,這一點很重要,由於JIT對這些模式進行了更多的優化。在一些具備數百個複雜正則表達式的實際工做負載上,將它們組合起來能夠將JIT表達式所花的時間減小20%以上。

Threading and Async


net 5中關於異步的最大變化之一其實是默認不啓用的,但這是另外一個得到反饋的實驗。net 5中的異步ValueTask池博客更詳細地解釋,但本質上dotnet/coreclr#26310介紹了異步ValueTask能力和異步ValueTask 隱式建立的緩存和重用對象表明一個異步操做完成,使得這些方法amortized-allocation-free的開銷。優化目前是可選的,這意味着您須要將DOTNET_SYSTEM_THREADING_POOLASYNCVALUETASKS環境變量設置爲1才能啓用它。啓用這一功能的困難之一是,對於可能要執行比等待SomeValueTaskReturningMethod()更復雜的操做的代碼,由於valuetask比任務有更多關於如何使用它們的約束。爲了幫助解決這個問題,一種新的UseValueTasksCorrectly分析儀發佈了,它將標記大多數此類誤用。

[Benchmark]
public async Task ValueTaskCost()
{
    for (int i = 0; i < 1_000; i++)
        await YieldOnce();
}

private static async ValueTask YieldOnce() => await Task.Yield();
Method Runtime Mean Ratio Allocated
ValueTaskCost .NET FW 4.8 1,635.6 us 1.00 294010 B
ValueTaskCost .NET Core 3.1 842.7 us 0.51 120184 B
ValueTaskCost .NET 5.0 812.3 us 0.50 186 B


c#編譯器中的一些變化爲.NET 5中的異步方法帶來了額外的好處(在 .NET5中的核心庫是用更新的編譯器編譯的)。每一個異步方法都有一個負責生成和完成返回任務的「生成器」,而c#編譯器將生成代碼做爲異步方法的一部分來使用。避免做爲代碼的一部分生成結構副本,這能夠幫助減小開銷,特別是對於async ValueTask方法,其中構建器相對較大(並隨着T的增加而增加)。一樣來自@benaadamsdotnet/roslyn#45262也調整了相同的生成代碼,以更好地發揮前面討論的JIT的零改進。
在特定的api中也有一些改進。dotnet/runtime#35575誕生於一些特定的任務使用Task.ContinueWith,其中延續純粹用於記錄「先行」任務continue from中的異常。一般狀況下,任務不會出錯,而PR在這種狀況下會作得更好。

const int Iters = 1_000_000;

private AsyncTaskMethodBuilder[] tasks = new AsyncTaskMethodBuilder[Iters];

[IterationSetup]
public void Setup()
{
    Array.Clear(tasks, 0, tasks.Length);
    for (int i = 0; i < tasks.Length; i++)
        _ = tasks[i].Task;
}

[Benchmark(OperationsPerInvoke = Iters)]
public void Cancel()
{
    for (int i = 0; i < tasks.Length; i++)
    {
        tasks[i].Task.ContinueWith(_ => { }, CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
        tasks[i].SetResult();
    }
}
Method Runtime Mean Ratio Allocated
Cancel .NET FW 4.8 239.2 ns 1.00 193 B
Cancel .NET Core 3.1 140.3 ns 0.59 192 B
Cancel .NET 5.0 106.4 ns 0.44 112 B


也有一些調整,以幫助特定的架構。因爲x86/x64架構採用了強內存模型,當針對x86/x64時,volatile在JIT時基本上就消失了。ARM/ARM64的狀況不是這樣,它的內存模型較弱,而且volatile會致使JIT發出圍欄。dotnet/runtime#36697刪除了每一個排隊到線程池的工做項的幾個volatile訪問,使ARM上的線程池更快。dotnet/runtime#34225將ConcurrentDictionary中的volatile訪問從一個循環中拋出,這反過來提升了ARM上ConcurrentDictionary的一些成員的吞吐量高達30%。而dotnet/runtime#36976則徹底從另外一個ConcurrentDictionary字段中刪除了volatile。

Collections


多年來,c#已經得到了大量有價值的特性。這些特性中的許多都是爲了讓開發人員可以更簡潔地編寫代碼,而語言/編譯器負責全部樣板文件,好比c# 9中的記錄。然而,有一些特性更注重性能而不是生產力,這些特性對核心庫來講是一個巨大的恩惠,它們能夠常用它們來提升每一個人的程序的效率。來自@benaadamsdotnet/runtime#27195就是一個很好的例子。PR改進了Dictionary<TKey, TValue>,利用了c# 7中引入的ref返回和ref局部變量。>的實現是由字典中的數組條目支持的,字典有一個核心例程用於在其條目數組中查找鍵的索引;而後在多個函數中使用該例程,如indexer、TryGetValue、ContainsKey等。可是,這種共享是有代價的:經過返回索引並將其留給調用者根據須要從槽中獲取數據,調用者將須要從新索引到數組中,從而致使第二次邊界檢查。有了ref返回,共享例程就能夠把一個ref遞迴給槽,而不是原始索引,這樣調用者就能夠避免第二次邊界檢查,同時也避免複製整個條目。PR還包括對生成的程序集進行一些低級調優、從新組織字段和用於更新這些字段的操做,以便JIT可以更好地調優生成的程序集。
字典<TKey,TValue>的性能進一步提升了幾個PRs。像許多哈希表同樣,Dictionary<TKey,TValue>被劃分爲「bucket」,每一個bucket本質上是一個條目鏈表(存儲在數組中,而不是每一個項都有單獨的節點對象)。對於給定的鍵,一個哈希函數(TKey ' s GetHashCode或提供的IComparer ' s GetHashCode)用於計算提供的鍵的哈希碼,而後該哈希碼肯定地映射到一個bucket;找到bucket以後,實現將遍歷該bucket中的條目鏈,查找目標鍵。該實現試圖保持每一個bucket中的條目數較小,並在必要時進行增加和從新平衡以維護該條件。所以,查找的很大一部分開銷是計算hashcode到bucket的映射。爲了幫助在bucket之間保持良好的分佈,特別是當提供的TKey或比較器使用不太理想的哈希代碼生成器時,字典使用質數的bucket,而bucket映射由hashcode % numBuckets完成。可是在這裏重要的速度,%操做符采用的除法是相對昂貴的。基於Daniel Lemire的工做,dotnet/coreclr#27299(來自@benaadams)和dotnet/runtime#406改變了64位進程中%的使用,而不是使用一對乘法和移位來實現相同的結果,但更快。

private Dictionary<int, int> _dictionary = Enumerable.Range(0, 10_000).ToDictionary(i => i);

[Benchmark]
public int Sum()
{
    Dictionary<int, int> dictionary = _dictionary;
    int sum = 0;

    for (int i = 0; i < 10_000; i++)
        if (dictionary.TryGetValue(i, out int value))
            sum += value;

    return sum;
}
Method Runtime Mean Ratio
Sum .NET FW 4.8 77.45 us 1.00
Sum .NET Core 3.1 67.35 us 0.87
Sum .NET 5.0 44.10 us 0.57


HashSet很是相似於Dictionary<TKey, TValue>。雖然它公開了一組不一樣的操做(沒有雙關的意思),除了只存儲一個鍵而不是一個鍵和一個值以外,它的數據結構基本上是相同的……或者至少過去是同樣的。多年來,考慮到使用Dictionary<TKey,TValue>比HashSet多多少,咱們花費了更多的努力來優化Dictionary<TKey,TValue>的實現,這兩種實現已經漂移了。dotnet/corefx#40106 @JeffreyZhao移植的一些改進詞典散列集,而後dotnet/runtime#37180有效地改寫HashSet 的實施經過re-syncing字典的(連同低堆棧移動,一些地方字典被用於一組被妥善取代)。最終的結果是HashSet最終得到了相似的收益(甚至更多,由於它是從一個更糟糕的地方開始的)。

private HashSet<int> _set = Enumerable.Range(0, 10_000).ToHashSet();

[Benchmark]
public int Sum()
{
    HashSet<int> set = _set;
    int sum = 0;

    for (int i = 0; i < 10_000; i++)
        if (set.Contains(i))
            sum += i;

    return sum;
}
Method Runtime Mean Ratio
Sum .NET FW 4.8 76.29 us 1.00
Sum .NET Core 3.1 79.23 us 1.04
Sum .NET 5.0 42.63 us 0.56


相似地,dotnet/runtime#37081移植了相似的改進,從Dictionary<TKey, TValue>到ConcurrentDictionary<TKey, TValue>。

private ConcurrentDictionary<int, int> _dictionary = new ConcurrentDictionary<int, int>(Enumerable.Range(0, 10_000).Select(i => new KeyValuePair<int, int>(i, i)));

[Benchmark]
public int Sum()
{
    ConcurrentDictionary<int, int> dictionary = _dictionary;
    int sum = 0;

    for (int i = 0; i < 10_000; i++)
        if (dictionary.TryGetValue(i, out int value))
            sum += value;

    return sum;
}
Method Runtime Mean Ratio
Sum .NET FW 4.8 115.25 us 1.00
Sum .NET Core 3.1 84.30 us 0.73
Sum .NET 5.0 49.52 us 0.43


System.Collections。不可變的版本也有改進。dotnet/runtime#1183@hnrqbaggio經過添加[MethodImpl(methodimploptions.ancsiveinlining)]到ImmutableArray的GetEnumerator方法來提升對ImmutableArray的GetEnumerator方法的foreach性能。咱們一般很是謹慎灑AggressiveInlining:它能夠使微基準測試看起來很好,由於它最終消除調用相關方法的開銷,但它也能夠大大提升代碼的大小,而後一大堆事情產生負面影響,如致使指令緩存變得不那麼有效了。然而,在這種狀況下,它不只提升了吞吐量,並且實際上還減小了代碼的大小。內聯是一種強大的優化,不只由於它消除了調用的開銷,還由於它向調用者公開了被調用者的內容。JIT一般不作過程間分析,這是因爲JIT用於優化的時間預算有限,可是內聯經過合併調用者和被調用者克服了這一點,在這一點上調用者因素的JIT優化被調用者因素。假設一個方法public static int GetValue() => 42;調用者執行if (GetValue() * 2 > 100){…不少代碼…}。若是GetValue()沒有內聯,那麼比較和「大量代碼」將會被JIT處理,可是若是GetValue()內聯,JIT將會看到這就像(84 > 100){…不少代碼…},則整個塊將被刪除。幸運的是,這樣一個簡單的方法幾乎老是會自動內聯,可是ImmutableArray的GetEnumerator足夠大,JIT沒法自動識別它的好處。在實踐中,當內聯GetEnumerator時,JIT最終可以更好地識別出foreach在遍歷數組,而不是爲Sum生成代碼:

; Program.Sum()
       push      rsi
       sub       rsp,30
       xor       eax,eax
       mov       [rsp+20],rax
       mov       [rsp+28],rax
       xor       esi,esi
       cmp       [rcx],ecx
       add       rcx,8
       lea       rdx,[rsp+20]
       call      System.Collections.Immutable.ImmutableArray'1[[System.Int32, System.Private.CoreLib]].GetEnumerator()
       jmp       short M00_L01
M00_L00:
       cmp       [rsp+28],edx
       jae       short M00_L02
       mov       rax,[rsp+20]
       mov       edx,[rsp+28]
       movsxd    rdx,edx
       mov       eax,[rax+rdx*4+10]
       add       esi,eax
M00_L01:
       mov       eax,[rsp+28]
       inc       eax
       mov       [rsp+28],eax
       mov       rdx,[rsp+20]
       mov       edx,[rdx+8]
       cmp       edx,eax
       jg        short M00_L00
       mov       eax,esi
       add       rsp,30
       pop       rsi
       ret
M00_L02:
       call      CORINFO_HELP_RNGCHKFAIL
       int       3
; Total bytes of code 97


就像在.NET Core 3.1中同樣,在.NET 5中也是如此

; Program.Sum()
       sub       rsp,28
       xor       eax,eax
       add       rcx,8
       mov       rdx,[rcx]
       mov       ecx,[rdx+8]
       mov       r8d,0FFFFFFFF
       jmp       short M00_L01
M00_L00:
       cmp       r8d,ecx
       jae       short M00_L02
       movsxd    r9,r8d
       mov       r9d,[rdx+r9*4+10]
       add       eax,r9d
M00_L01:
       inc       r8d
       cmp       ecx,r8d
       jg        short M00_L00
       add       rsp,28
       ret
M00_L02:
       call      CORINFO_HELP_RNGCHKFAIL
       int       3
; Total bytes of code 59


所以,更小的代碼和更快的執行:

private ImmutableArray<int> _array = ImmutableArray.Create(Enumerable.Range(0, 100_000).ToArray());

[Benchmark]
public int Sum()
{
    int sum = 0;

    foreach (int i in _array)
        sum += i;

    return sum;
}
Method Runtime Mean Ratio
Sum .NET FW 4.8 187.60 us 1.00
Sum .NET Core 3.1 187.32 us 1.00
Sum .NET 5.0 46.59 us 0.25


ImmutableList 。包含也看到了顯著的改進,因爲來自 @shortspiderdotnet/corefx#40540。Contains是使用ImmutableList的IndexOf方法實現的,這個方法是在它的枚舉器上實現的。在幕後ImmutableList 今天AVL樹,實現自平衡的二叉查找樹的一種形式,爲了走這樣的樹,它須要保持一個非平凡的狀態,和ImmutableList 的枚舉器去煞費苦心每一個枚舉爲了不分配存儲。這致使了不小的開銷。可是,Contains並不關心列表中元素的確切索引(也不關心找到了可能的多個副本中的哪一個副本),只關心它的存在,所以,它能夠使用簡單的遞歸樹搜索。(由於樹是平衡的,因此咱們不關心堆棧溢出條件。)

private ImmutableList<int> _list = ImmutableList.Create(Enumerable.Range(0, 1_000).ToArray());

[Benchmark]
public int Sum()
{
    int sum = 0;

    for (int i = 0; i < 1_000; i++)
        if (_list.Contains(i))
            sum += i;

    return sum;
}
Method Runtime Mean Ratio
Sum .NET FW 4.8 22.259 ms 1.00
Sum .NET Core 3.1 22.872 ms 1.03
Sum .NET 5.0 2.066 ms 0.09


前面強調的集合改進都是針對通用集合的,即用於開發人員須要存儲的任何數據。但並非全部的集合類型都是這樣的:有些更專門用於特定的數據類型,而這樣的集合在。net 5中也能夠看到性能的改進。位數組就是這樣的一個例子,與幾個PRs這個釋放做出重大改進,以其性能。特別地,來自@Gnbrkm41dotnet/corefx#41896使用了AVX2和SSE2特性來對BitArray的許多操做進行矢量化(dotnet/runtime#33749隨後也添加了ARM64特性):

private bool[] _array;

[GlobalSetup]
public void Setup()
{
    var r = new Random(42);
    _array = Enumerable.Range(0, 1000).Select(_ => r.Next(0, 2) == 0).ToArray();
}

[Benchmark]
public BitArray Create() => new BitArray(_array);
Method Runtime Mean Ratio
Create .NET FW 4.8 1,140.91 ns 1.00
Create .NET Core 3.1 861.97 ns 0.76
Create .NET 5.0 49.08 ns 0.04

LINQ


在.NET Core以前的版本中,系統出現了大量的變更。Linq代碼基,特別是提升性能。這個流程已經放緩了,可是.NET 5仍然能夠看到LINQ的性能改進。
OrderBy有一個值得注意的改進。正如前面所討論的,將coreclr的本地排序實現轉換爲託管代碼有多種動機,其中一個就是可以輕鬆地將其做爲基於spanc的排序方法的一部分進行重用。這樣的api是公開的,而且經過dotnet/runtime#1888,咱們可以在System.Linq中利用基於spane的排序。這特別有好處,由於它支持利用基於Comparison的排序例程,這反過來又支持避免在每一個比較操做上的多層間接。

[GlobalSetup]
public void Setup()
{
    var r = new Random(42);
    _array = Enumerable.Range(0, 1_000).Select(_ => r.Next()).ToArray();
}

private int[] _array;

[Benchmark]
public void Sort()
{
    foreach (int i in _array.OrderBy(i => i)) { }
}
Method Runtime Mean Ratio
Sort .NET FW 4.8 100.78 us 1.00
Sort .NET Core 3.1 101.03 us 1.00
Sort .NET 5.0 85.46 us 0.85


對於一行更改來講,這還不錯。
另外一個改進是來自@timandydotnet/corefx#41342。PR可擴充的枚舉。SkipLast到特殊狀況IList以及內部IPartition接口(這是各類操做符相互之間進行優化的方式),以便在能夠廉價肯定源長度時將SkipLast從新表示爲Take操做。

private IEnumerable<int> data = Enumerable.Range(0, 100).ToList();

[Benchmark]
public int SkipLast() => data.SkipLast(5).Sum();
Method Runtime Mean Ratio Allocated
SkipLast .NET Core 3.1 1,641.0 ns 1.00 248 B
SkipLast .NET 5.0 684.8 ns 0.42 48 B


最後一個例子,dotnet/corefx#40377是一個漫長的過程。這是一個有趣的例子。一段時間以來,我看到開發人員認爲Enumerable.Any()比Enumerable.Count() != 0更有效;畢竟,Any()只須要肯定源中是否有東西,而Count()須要肯定源中有多少東西。所以,對於任何合理的集合,any()在最壞狀況下應該是O(1),而Count()在最壞狀況下多是O(N),那麼any()不是老是更好的嗎?甚至有Roslyn分析程序推薦這種轉換。不幸的是,狀況並不老是這樣。在。net 5以前,Any()的實現基本以下:

using (IEnumerator<T> e = source.GetEnumerator)
    return e.MoveNext();


這意味着在一般狀況下,即便多是O(1)操做,也會致使分配一個枚舉器對象以及兩個接口分派。相比之下,自從. net Framework 3.0中LINQ的初始版本發佈以來,Count()已經優化了特殊狀況下ICollection使用它的Count屬性的代碼路徑,在這種狀況下,它一般是O(1)和分配自由,只有一個接口分派。所以,對於很是常見的狀況(好比源是List),使用Count() != 0實際上比使用Any()更有效。雖然添加接口檢查會帶來一些開銷,但值得添加它以使Any()實現具備可預測性並與Count()保持一致,這樣就能夠更容易地對其進行推理,並使有關其成本的主流觀點變得正確。

Networking


現在,網絡是幾乎全部應用程序的關鍵組件,而良好的網絡性能相當重要。所以,.NET的每個版本都在提升網絡性能上投入了大量的精力.NET 5也不例外。
讓咱們先看看一些原語,而後繼續往下看。系統。大多數應用程序都使用Uri來表示url,它的速度要快,這一點很重要。許多PRs已經開始在。.NET 5中使Uri更快。能夠說,Uri最重要的操做是構造一個Uri,而dotnet/runtime#36915使全部Uri的構造速度更快,主要是經過關注開銷和避免沒必要要的開銷:

[Benchmark]
public Uri Ctor() => new Uri("https://github.com/dotnet/runtime/pull/36915");
Method Runtime Mean Ratio Allocated
Ctor .NET FW 4.8 443.2 ns 1.00 225 B
Ctor .NET Core 3.1 192.3 ns 0.43 72 B
Ctor .NET 5.0 129.9 ns 0.29 56 B


在構造以後,應用程序常常訪問Uri的各類組件,這一點也獲得了改進。特別是,像HttpClient這樣的類型一般有一個重複用於發出請求的Uri。HttpClient實現將訪問Uri。屬性的路徑和查詢,以發送做爲HTTP請求的一部分(例如,GET /dotnet/runtime HTTP/1.1),在過去,這意味着爲每一個請求從新建立Uri的部分字符串。感謝dotnet/runtime#36460,它如今被緩存(就像IdnHost同樣):

private Uri _uri = new Uri("http://github.com/dotnet/runtime");

[Benchmark]
public string PathAndQuery() => _uri.PathAndQuery;
Method Runtime Mean Ratio Allocated
PathAndQuery .NET FW 4.8 17.936 ns 1.00 56 B
PathAndQuery .NET Core 3.1 30.891 ns 1.72 56 B
PathAndQuery .NET 5.0 2.854 ns 0.16


除此以外,還有許多代碼與uri交互的方式,其中許多都獲得了改進。例如,dotnet/corefx#41772改進了Uri。EscapeDataString和Uri。EscapeUriString,它根據RFC 3986RFC 3987對字符串進行轉義。這兩種方法都依賴於使用不安全代碼的共享 helpers,經過char[]來回切換,而且在Unicode處理方面有不少複雜性。這個PR重寫了這個 helpers來利用.NET的新特性,好比span和符文,以使escape操做既安全又快速。對於某些輸入,增益不大,可是對於涉及Unicode的輸入,甚至對於長ASCII輸入,增益就很大了。

[Params(false, true)]
public bool ASCII { get; set; }

[GlobalSetup]
public void Setup()
{
    _input = ASCII ?
        new string('s', 20_000) :
        string.Concat(Enumerable.Repeat("\xD83D\xDE00", 10_000));
}

private string _input;

[Benchmark] public string Escape() => Uri.EscapeDataString(_input);
Method Runtime ASCII Mean Ratio Allocated
Escape .NET FW 4.8 False 6,162.59 us 1.00 60616272 B
Escape .NET Core 3.1 False 6,483.85 us 1.06 60612025 B
Escape .NET 5.0 False 243.09 us 0.04 240045 B
Escape .NET FW 4.8 True 86.93 us 1.00
Escape .NET Core 3.1 True 122.06 us 1.40
Escape .NET 5.0 True 14.04 us 0.16


爲Uri.UnescapeDataString提供了相應的改進。這一改變包括使用已經向量化的IndexOf而不是手動的基於指針的循環,以肯定須要進行非轉義的字符的第一個位置,而後避免一些沒必要要的代碼,並在可行的狀況下使用堆棧分配而不是堆分配。雖然使全部操做更快,最大的收益是字符串unescape無關,這意味着EscapeDataString操做沒有逃避,只是返回其輸入(這種狀況也隨後幫助進一步dotnet/corefx#41684,使原來的字符串返回時不須要改變):

private string _value = string.Concat(Enumerable.Repeat("abcdefghijklmnopqrstuvwxyz", 20));

[Benchmark]
public string Unescape() => Uri.UnescapeDataString(_value);
Method Runtime Mean Ratio
Unescape .NET FW 4.8 847.44 ns 1.00
Unescape .NET Core 3.1 846.84 ns 1.00
Unescape .NET 5.0 21.84 ns 0.03


dotnet/runtime#36444dotnet/runtime#32713使比較uri和執行相關操做(好比將它們放入字典)變得更快,尤爲是相對uri。

private Uri[] _uris = Enumerable.Range(0, 1000).Select(i => new Uri($"/some/relative/path?ID={i}", UriKind.Relative)).ToArray();

[Benchmark]
public int Sum()
{
    int sum = 0;

    foreach (Uri uri in _uris)
        sum += uri.GetHashCode();
        
    return sum;
}
Method Runtime Mean Ratio
Sum .NET FW 4.8 330.25 us 1.00
Sum .NET Core 3.1 47.64 us 0.14
Sum .NET 5.0 18.87 us 0.06


向上移動堆棧,讓咱們看看System.Net.Sockets。自從.NET Core誕生以來,TechEmpower基準就被用做衡量進展的一種方式。之前咱們主要關注「明文」基準,很是低級的一組特定的性能特徵,但對於這個版本,咱們但願專一於改善兩個基準,「JSON序列化」和「財富」(後者涉及數據庫訪問,儘管它的名字,前者的成本主要是因爲網絡速度很是小的JSON載荷有關)。咱們的工做主要集中在Linux上。當我說「咱們的」時,我不只僅是指那些在.NET團隊工做的人;咱們經過一個超越核心團隊的工做小組進行了富有成效的合做,例如紅帽的@tmds和Illyriad Games的@benaadams的偉大想法和貢獻。


在Linux上,socket實現是基於epoll的。爲了實現對許多服務的巨大需求,咱們不能僅僅爲每一個套接字分配一個線程,若是對套接字上的全部操做都使用阻塞I/O,咱們就會這樣作。相反,使用非阻塞I/O,當操做系統尚未準備好來知足一個請求(例如當ReadAsync用於套接字但沒有數據可供閱讀,或使用非同步套接字可是沒有可用空間在內核的發送緩衝區),epoll用於通知套接字實現的套接字狀態的變化,這樣操做能夠再次嘗試。epoll是一種使用一個線程有效地阻塞任何數量套接字的更改等待的方法,所以實現維護了一個專用的線程,等待更改的全部套接字註冊的epoll。該實現維護了多個epoll線程,這些線程的數量一般等於系統中內核數量的一半。當多個套接字都複用到同一個epoll和epoll線程時,實現須要很是當心,不要在響應套接字通知時運行任意的工做;這樣作會發生在epoll線程自己,所以epoll線程將沒法處理進一步的通知,直到該工做完成。更糟糕的是,若是該工做被阻塞,等待與同一epoll關聯的任何套接字上的另外一個通知,系統將死鎖。所以,處理epoll的線程試圖在響應套接字通知時作儘量少的工做,提取足夠的信息將實際處理排隊到線程池中。


事實證實,在這些epoll線程和線程池之間發生了一個有趣的反饋循環。來自epoll線程的工做項排隊的開銷恰好足夠支持多個epoll線程,可是多個epoll線程會致使隊列發生一些爭用,以致於每一個額外的線程所增長的開銷都超過了它的公平份額。最重要的是,排隊的速度只是足夠低,線程池將很難保持它的全部線程飽和的狀況下會發生少許的工做在一個套接字操做(這是JSON序列化基準的狀況);這將反過來致使線程池花費更多的時間來隔離和釋放線程,從而使其變慢,從而建立一個反饋循環。長話短說,不理想的排隊會致使較慢的處理速度和比實際須要更多的epoll線程。這被糾正與兩個PRs, dotnet/runtime#35330dotnet/runtime#35800。#35330改變了從epoll線程排隊模型,而不是排隊一個工做項/事件(當epoll醒來通知,可能會有多個通知全部的套接字註冊它,和它將提供全部的通知在一批),它將整個批處理隊列的一個工做項。處理它的池線程而後使用一個很是相似於並行的模型。For/ForEach已經工做多年,也就是說,排隊的工做項能夠爲本身保留一個項,而後將本身的副本排隊以幫助處理剩餘的項。這改變了微積分,最合理大小的機器,它實際上成爲有利於減小epoll線程而不是更多(並不是巧合的是,咱們但願有更少的),那麼# 35800 epoll線程的數量變化,一般使用最終只是一個(在機器與更大的核心方面,還會有更多)。咱們還經過經過DOTNET_SYSTEM_NET_SOCKETS_THREAD_COUNT epoll數可配置環境變量,能夠設置爲所需的計算以覆蓋系統的默認值,若是開發人員想要實驗與其餘數量和提供反饋結果給定的工做負載。


做爲一個實驗,從@tmds dotnet/runtime#37974咱們還添加了一個實驗模式(由DOTNET_SYSTEM_NET_SOCKETS_INLINE_COMPLETIONS環境變量設置爲1在Linux上)咱們避免排隊的工做線程池,而不是僅僅運行全部套接字延續(如工做()等待socket.ReadAsync ();工做()😉;在epoll線程上。嗝是我德拉古!若是套接字延續中止,則不會處理與該epoll線程關聯的其餘工做。更糟糕的是,若是延續實際上同步阻塞等待與該epoll關聯的其餘工做,系統將死鎖。可是,在這種模式下,一個精心設計的程序可能會得到更好的性能,由於處理的位置能夠更好,而且能夠避免排隊到線程池的開銷。由於全部套接字工做都在epoll線程上運行,因此默認爲1再也不有意義;默認狀況下,它的線程數等於處理器數。再說一次,這是一個實驗,咱們歡迎你看到任何積極或消極的結果。


這些改進都大規模地集中在Linux上的套接字性能上,這使得它們很難在單機上的微基準測試中進行演示。不過,還有其餘更容易看到的改進dotnet/runtime#32271從套接字刪除了幾個分配。鏈接,插座。爲了支持再也不相關的舊代碼訪問安全(CAS)檢查,對某些狀態進行了沒必要要的複製:CAS檢查在好久之前就被刪除了,可是克隆仍然存在,因此這也只是清理了它們。dotnet/runtime#32275也從SafeSocketHandle的Windows實現中刪除了一個分配。dotnet/runtime#787重構插座。ConnectAsync,以便它能夠共享相同的內部SocketAsyncEventArgs實例,該實例最終被隨後用於執行ReceiveAsync操做,從而避免額外的鏈接分配。dotnet /運行時# 34175利用.NET 5中引入的新的固定對象堆使用pre-pinned緩衝區SocketAsyncEventArgs實現的各部分在Windows上而不是用GCHandle銷(在Linux上不須要把相應的功能,因此它是不習慣)。在dotnet/runtime#37583中,@tmds經過在適當的地方使用堆棧分配,減小了做爲向生I/O SendAsync/ReceivedAsync實現的一部分的分配。

private Socket _listener, _client, _server;
private byte[] _buffer = new byte[8];
private List<ArraySegment<byte>> _buffers = new List<ArraySegment<byte>>();

[GlobalSetup]
public void Setup()
{
    _listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    _listener.Bind(new IPEndPoint(IPAddress.Loopback, 0));
    _listener.Listen(1);

    _client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    _client.ConnectAsync(_listener.LocalEndPoint).GetAwaiter().GetResult();

    _server = _listener.AcceptAsync().GetAwaiter().GetResult();

    for (int i = 0; i < _buffer.Length; i++)
        _buffers.Add(new ArraySegment<byte>(_buffer, i, 1));
}

[Benchmark]
public async Task SendReceive()
{
    await _client.SendAsync(_buffers, SocketFlags.None);
    int total = 0;
    while (total < _buffer.Length)
        total += await _server.ReceiveAsync(_buffers, SocketFlags.None);
}
Method Runtime Mean Ratio Allocated
SendReceive .NET Core 3.1 5.924 us 1.00 624 B
SendReceive .NET 5.0 5.230 us 0.88 144 B


在此之上,咱們來到System.Net.Http。SocketsHttpHandler在兩個方面作了大量改進。第一個是頭的處理,它表明了與類型相關的分配和處理的很大一部分。經過建立HttpHeaders, dotnet/corefx#41640啓動了事情。TryAddWithoutValidation的名稱爲真:因爲SocketsHttpHandler枚舉請求頭並將它們寫入連線的方式,即便開發人員指定了「WithoutValidation」,它最終仍是會對頭執行驗證,PR修復了這個問題。多個PRs,包括dotnet/runtime#35003dotnet/runtime#34922dotnet/runtime#32989dotnet/runtime#34974改進了在SocketHttpHandler的已知標頭列表中的查找(當這些標頭出現時,這有助於避免分配),並加強了該列表以更加全面。dotnet/runtime#34902更新內部各強類型集合類型使用頭少分配集合,和dotnet/runtime#34724作了一些相關的分配頭到手只有當他們實際上訪問(以及特殊狀況的日期和服務器響應標頭以免爲他們分配在最多見的狀況下)。最終的結果是吞吐量獲得了小的改善,但分配獲得了顯著的改善:

private static readonly Socket s_listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
private static readonly HttpClient s_client = new HttpClient();
private static Uri s_uri;

[Benchmark]
public async Task HttpGet()
{
    var m = new HttpRequestMessage(HttpMethod.Get, s_uri);
    m.Headers.TryAddWithoutValidation("Authorization", "ANYTHING SOMEKEY");
    m.Headers.TryAddWithoutValidation("Referer", "http://someuri.com");
    m.Headers.TryAddWithoutValidation("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36");
    m.Headers.TryAddWithoutValidation("Host", "www.somehost.com");
    using (HttpResponseMessage r = await s_client.SendAsync(m, HttpCompletionOption.ResponseHeadersRead))
    using (Stream s = await r.Content.ReadAsStreamAsync())
        await s.CopyToAsync(Stream.Null);
}

[GlobalSetup]
public void CreateSocketServer()
{
    s_listener.Bind(new IPEndPoint(IPAddress.Loopback, 0));
    s_listener.Listen(int.MaxValue);
    var ep = (IPEndPoint)s_listener.LocalEndPoint;
    s_uri = new Uri($"http://{ep.Address}:{ep.Port}/");
    byte[] response = Encoding.UTF8.GetBytes("HTTP/1.1 200 OK\r\nDate: Sun, 05 Jul 2020 12:00:00 GMT \r\nServer: Example\r\nContent-Length: 5\r\n\r\nHello");
    byte[] endSequence = new byte[] { (byte)'\r', (byte)'\n', (byte)'\r', (byte)'\n' };

    Task.Run(async () =>
    {
        while (true)
        {
            Socket s = await s_listener.AcceptAsync();
            _ = Task.Run(() =>
            {
                using (var ns = new NetworkStream(s, true))
                {
                    byte[] buffer = new byte[1024];
                    int totalRead = 0;
                    while (true)
                    {
                        int read =  ns.Read(buffer, totalRead, buffer.Length - totalRead);
                        if (read == 0) return;
                        totalRead += read;
                        if (buffer.AsSpan(0, totalRead).IndexOf(endSequence) == -1)
                        {
                            if (totalRead == buffer.Length) Array.Resize(ref buffer, buffer.Length * 2);
                            continue;
                        }

                        ns.Write(response, 0, response.Length);

                        totalRead = 0;
                    }
                }
            });
        }
    });
}
Method Runtime Mean Ratio Allocated
HttpGet .NET FW 4.8 123.67 us 1.00 98.48 KB
HttpGet .NET Core 3.1 68.57 us 0.55 6.07 KB
HttpGet .NET 5.0 66.80 us 0.54 2.86 KB


其餘一些與主管有關的PRs更爲專業化。例如,dotnet/runtime#34860經過更仔細地考慮方法改進了日期頭的解析。前面的實現使用的是DateTime。一長串可行格式的TryParseExact;這就使實現失去了它的快速路徑,而且致使即便輸入與列表中的第一種格式匹配時,解析它的速度也要慢得多。在今天的日期標題中,絕大多數標題將遵循RFC 1123中列出的格式,也就是「r」。因爲以前版本的改進,DateTime對「r」格式的解析很是快,因此咱們能夠先直接使用TryParseExact對單一格式進行解析,若是它失敗了,就使用TryParseExact對其他格式進行解析。

[Benchmark]
public DateTimeOffset? DatePreferred()
{
    var m = new HttpResponseMessage();
    m.Headers.TryAddWithoutValidation("Date", "Sun, 06 Nov 1994 08:49:37 GMT");
    return m.Headers.Date;
}
Method Runtime Mean Ratio Allocated
DatePreferred .NET FW 4.8 2,177.9 ns 1.00 674 B
DatePreferred .NET Core 3.1 1,510.8 ns 0.69 544 B
DatePreferred .NET 5.0 267.2 ns 0.12 520 B


然而,最大的改進來自於通常的HTTP/2。在.NET Core 3.1中,HTTP/2實現是功能性的,但沒有進行特別的調優,因此在.NET5上作了一些努力,使HTTP/2實現更好,特別是更具備可伸縮性。dotnet/runtime#32406dotnet/runtime#32624顯著下降分配參與HTTP/2 GET請求經過使用一個自定義CopyToAsync覆蓋在響應流用於HTTP/2響應,被更當心在如何訪問請求頭寫請求的一部分(爲了不迫使lazily-initialized狀態存在的時候沒有必要),和刪除async-related分配。而dotnet/runtime#32557減小了HTTP/2中的分配,經過更好地處理取消和減小與異步操做相關的分配。之上,dotnet/runtime#35694包括一堆HTTP /兩個相關的變化,包括減小鎖的數量涉及(HTTP/2涉及更多的同步比HTTP/1.1 c#實現,由於在HTTP / 2多個請求多路複用到相同的套接字鏈接),減小工做的數量,而持有鎖,一個關鍵的狀況下改變使用的鎖定機制,增長標題的標題優化,以及其餘一些減小管理費用的調整。做爲後續,dotnet/runtime#36246刪除了一些因爲取消和尾部標頭(這在gRPC流量中很常見)而形成的分配。爲了演示這一點,我建立了一個簡單的ASP.NET Core localhost服務器(使用空模板,刪除少許代碼,本例不須要):

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Hosting;

public class Program
{
    public static void Main(string[] args) =>
        Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(b => b.UseStartup<Startup>()).Build().Run();
}

public class Startup
{
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseRouting();
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapGet("/", context => context.Response.WriteAsync("Hello"));
            endpoints.MapPost("/", context => context.Response.WriteAsync("Hello"));
        });
    }
}


而後我使用這個客戶端基準:

private HttpMessageInvoker _client = new HttpMessageInvoker(new SocketsHttpHandler() { UseCookies = false, UseProxy = false, AllowAutoRedirect = false });
private HttpRequestMessage _get = new HttpRequestMessage(HttpMethod.Get, new Uri("https://localhost:5001/")) { Version = HttpVersion.Version20 };
private HttpRequestMessage _post = new HttpRequestMessage(HttpMethod.Post, new Uri("https://localhost:5001/")) { Version = HttpVersion.Version20, Content = new ByteArrayContent(Encoding.UTF8.GetBytes("Hello")) };

[Benchmark] public Task Get() => MakeRequest(_get);

[Benchmark] public Task Post() => MakeRequest(_post);

private Task MakeRequest(HttpRequestMessage request) => Task.WhenAll(Enumerable.Range(0, 100).Select(async _ =>
{
    for (int i = 0; i < 500; i++)
    {
        using (HttpResponseMessage r = await _client.SendAsync(request, default))
        using (Stream s = await r.Content.ReadAsStreamAsync())
            await s.CopyToAsync(Stream.Null);
    }
}));
Method Runtime Mean Ratio Allocated
Get .NET Core 3.1 1,267.4 ms 1.00 122.76 MB
Get .NET 5.0 681.7 ms 0.54 74.01 MB
Post .NET Core 3.1 1,464.7 ms 1.00 280.51 MB
Post .NET 5.0 735.6 ms 0.50 132.52 MB


還要注意的是,對於.NET 5,在這方面還有不少工做要作。dotnet/runtime#38774改變了在HTTP/2實現中處理寫的方式,預計將在已有改進的基礎上帶來實質性的可伸縮性提升,特別是針對基於grpc的工做負載。
其餘網絡組件也有顯著的改進。例如,Dns類型上的XxAsync api是在相應的Begin/EndXx方法上實現的。對於.NET 5中的dotnet/corefx#41061,這是反向的,例如Begin/EndXx方法是在XxAsync方法的基礎上實現的;這使得代碼更簡單、更快,同時對分配也有很好的影響(注意.NET Framework 4.8的結果稍微快一些,由於它實際上沒有使用異步I/O,而只是一個排隊的工做項到執行同步I/O的線程池;這樣會減小一些開銷,但也會減小可伸縮性):

private string _hostname = Dns.GetHostName();

[Benchmark] public Task<IPAddress[]> Lookup() => Dns.GetHostAddressesAsync(_hostname);
Method Runtime Mean Ratio Allocated
Lookup .NET FW 4.8 178.6 us 1.00 4146 B
Lookup .NET Core 3.1 211.5 us 1.18 1664 B
Lookup .NET 5.0 209.7 us 1.17 984 B


雖然是一種不多有人(儘管它使用WCF), NegotiateStream也一樣更新dotnet/runtime#36583,與全部XxAsync方法被使用異步/等待,而後在dotnet/runtime#37772複用緩衝區,而不是爲每一個操做建立新的。最終結果是在典型的讀/寫使用中顯著減小分配:

private byte[] _buffer = new byte[1];
private NegotiateStream _client, _server;

[GlobalSetup]
public void Setup()
{
    using var listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    listener.Bind(new IPEndPoint(IPAddress.Loopback, 0));
    listener.Listen(1);

    var client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    client.ConnectAsync(listener.LocalEndPoint).GetAwaiter().GetResult();

    Socket server = listener.AcceptAsync().GetAwaiter().GetResult();

    _client = new NegotiateStream(new NetworkStream(client, true));
    _server = new NegotiateStream(new NetworkStream(server, true));

    Task.WaitAll(
        _client.AuthenticateAsClientAsync(),
        _server.AuthenticateAsServerAsync());
}

[Benchmark]
public async Task WriteRead()
{
    for (int i = 0; i < 100; i++)
    {
        await _client.WriteAsync(_buffer);
        await _server.ReadAsync(_buffer);
    }
}

[Benchmark]
public async Task ReadWrite()
{
    for (int i = 0; i < 100; i++)
    {
        var r = _server.ReadAsync(_buffer);
        await _client.WriteAsync(_buffer);
        await r;
    }
}
Method Runtime Mean Ratio Allocated
WriteRead .NET Core 3.1 1.510 ms 1.00 61600 B
WriteRead .NET 5.0 1.294 ms 0.86
ReadWrite .NET Core 3.1 3.502 ms 1.00 76224 B
ReadWrite .NET 5.0 3.301 ms 0.94 226 B

JSON


這個系統有了顯著的改進.NET 5的Json庫,特別是JsonSerializer,可是不少這些改進實際上都被移植回了.NET Core 3.1,並做爲服務修復的一部分發布(參見dotnet/corefx#41771)。即使如此,在.NET 5中也出現了一些不錯的改進。
dotnet/runtime#2259重構了JsonSerializer中的轉換器如何處理集合的模型,致使了可測量的改進,特別是對於更大的集合:

private MemoryStream _stream = new MemoryStream();
private DateTime[] _array = Enumerable.Range(0, 1000).Select(_ => DateTime.UtcNow).ToArray();

[Benchmark]
public Task LargeArray()
{
    _stream.Position = 0;
    return JsonSerializer.SerializeAsync(_stream, _array);
}
Method Runtime Mean Ratio Allocated
LargeArray .NET FW 4.8 262.06 us 1.00 24256 B
LargeArray .NET Core 3.1 191.34 us 0.73 24184 B
LargeArray .NET 5.0 69.40 us 0.26 152 B


但即便是較小的,例如。

private MemoryStream _stream = new MemoryStream();
private JsonSerializerOptions _options = new JsonSerializerOptions();
private Dictionary<string, int> _instance = new Dictionary<string, int>()
{
    { "One", 1 }, { "Two", 2 }, { "Three", 3 }, { "Four", 4 }, { "Five", 5 },
    { "Six", 6 }, { "Seven", 7 }, { "Eight", 8 }, { "Nine", 9 }, { "Ten", 10 },
};

[Benchmark]
public async Task Dictionary()
{
    _stream.Position = 0;
    await JsonSerializer.SerializeAsync(_stream, _instance, _options);
}
Method Runtime Mean Ratio Allocated
Dictionary .NET FW 4.8 2,141.7 ns 1.00 209 B
Dictionary .NET Core 3.1 1,376.6 ns 0.64 208 B
Dictionary .NET 5.0 726.1 ns 0.34 152 B


dotnet/runtime#37976還經過添加緩存層來幫助檢索被序列化和反序列化的類型內部使用的元數據,從而幫助提升小型類型的性能。

private MemoryStream _stream = new MemoryStream();
private MyAwesomeType _instance = new MyAwesomeType() { SomeString = "Hello", SomeInt = 42, SomeByte = 1, SomeDouble = 1.234 };

[Benchmark]
public Task SimpleType()
{
    _stream.Position = 0;
    return JsonSerializer.SerializeAsync(_stream, _instance);
}

public struct MyAwesomeType
{
    public string SomeString { get; set; }
    public int SomeInt { get; set; }
    public double SomeDouble { get; set; }
    public byte SomeByte { get; set; }
}
Method Runtime Mean Ratio Allocated
SimpleType .NET FW 4.8 1,204.3 ns 1.00 265 B
SimpleType .NET Core 3.1 617.2 ns 0.51 192 B
SimpleType .NET 5.0 504.2 ns 0.42 192 B

Trimming


在.NET Core 3.0以前,.NET Core主要關注服務器的工做負載,而ASP則主要關注服務器的工做負載。NET Core是該平臺上卓越的應用模型。隨着.NET Core 3.0的加入,Windows Forms和Windows Presentation Foundation (WPF)也隨之加入,將. NET Core引入到了桌面應用中。隨着.NET Core 3.2的發佈,Blazor發佈了對瀏覽器應用程序的支持,但它基於mono和mono堆棧中的庫。在.NET 5中,Blazor使用.NET 5 mono運行時和全部其餘應用模型共享的.NET 5庫。這給性能帶來了一個重要的變化:大小。在代碼大小一直是一個重要的問題(和.NET本機應用程序)是很是重要的,一個成功的基於瀏覽器的部署所需的規模確實帶來了最前沿,咱們須要擔憂下載大小在某種程度上咱們尚未過去集中與.NET Core。
協助與應用程序的大小,.NET SDK包含一個連接器,可以清除的未使用部分應用,不只在彙編級,但也在會員級別,作靜態分析來肯定什麼是代碼,不是使用和丟棄的部分不是。這帶來了一組有趣的挑戰:爲了方便或簡化API使用而採用的一些編碼模式,對於連接器來講,很難以容許它扔掉不少東西的方式進行分析。所以,在.NET 5中與性能相關的主要工做之一就是改進庫的可剪裁。


這有兩個方面:

  • 沒有刪除太多(正確性)。咱們須要確保這些庫可以真正安全地進行裁減。特別是,反射(甚至只反映在公共面積)的連接器很難找到全部成員,實際上可能被使用,如在應用程序代碼在一個地方使用typeof類型實例,並傳遞到另外一個應用程序的一部分,它使用GetMethod檢索MethodInfo對於一個公共方法,類型,並經過MethodInfo到另外一個應用程序調用它的一部分。地址,連接器採用啓發式方法來最大程度地減小能夠刪除的API的誤報,能夠刪除,但爲了進一步幫助它,一堆屬性添加了在.NET 5,使開發人員可以使這樣的隱式依賴關係顯式,抑制警告連接器在它可能認爲是不安全的,但實際上不是,轉嫁給消費者,迫使警告說表面的某些部分不適合鏈接。看到dotnet/runtime#35387
  • 儘量多地刪除(性能)。咱們須要儘可能減小代碼片斷須要保留的緣由。這能夠表現爲重構實現來改變調用模式,也能夠表現爲使用連接器能夠識別的使用條件來裁剪整段代碼,還能夠表現爲使用更細粒度的控制來精確地控制須要保留的內容和保留的緣由。


第二種方法有不少例子,因此我將着重介紹其中一些,以展現所使用的各類技術:

  • 刪除沒必要要的代碼,例如dotnet/corefx#41177。在這裏,咱們發現了許多過期的TraceSource/Switch用法,這些用法僅用於啓用一些僅用於調試的跟蹤和斷言,但實際上已經沒有人使用了,這致使連接器看到其中一些類型,甚至在發佈版本中也使用過。
  • 刪除曾經有用但再也不有用的過期代碼,例如dotnet/coreclr#26750。這種類型曾經對改進ngen (crossgen的前身)很重要,但如今不須要了。或者像在dotnet/coreclr#26603中,有些代碼實際上再也不使用,但仍然會致使類型保留下來。
  • 刪除重複的代碼,例如dotnet/corefx#41165dotnet/corefx#40935,和dotnet/coreclr#26589。一些庫使用它們本身的哈希代碼幫助例程的私有副本,致使每一個庫都有本身的IL副原本實現該功能。它們能夠被更新爲使用共享的HashCode類型,這不只有助於IL的大小和調整,還有助於避免須要維護的額外代碼,並更好地現代化代碼庫,以利用咱們建議其餘人也使用的功能。
  • 使用不一樣的api,例如dotnet/corefx#41143。代碼使用擴展幫助器方法,致使引入額外的類型,可是提供的「幫助」實際上幾乎沒有節省代碼。一個可能更好的示例是dotnet/corefx#41142,它從System.Xml實現中刪除了非通用隊列和堆棧類型的使用,而只是使用通用實現(dotnet/coreclr#26597使用WeakReference作了相似的事情)。或者dotnet/corefx#41111,它改變了XML庫中的一些代碼來使用HttpClient而不是WebRequest,這容許刪除整個System.Net。依賴的請求。或者避免System.Net的dotnet/corefx#41110。Http須要使用System.Text。RegularExpressions:這是沒必要要的複雜性,能夠用少許特定於該用例的代碼替換。另外一個例子是dotnet/coreclr#26602,其中一些代碼沒必要要地使用了string.ToLower(),替換它的使用不只更有效,並且有助於在默認狀況下削減重載。dotnet/coreclr#26601是類似的。
  • 從新路由邏輯以免對大量不須要的代碼進行根路由,例如dotnet/corefx#41075。若是代碼只是使用了新的Regex(字符串),那麼在內部只是委託給了更長的Regex(字符串,RegexOptions)構造函數,而且構造函數須要可以使用內部的RegexCompiler來應對RegexOptions。編譯使用。經過調整代碼路徑,使Regex(string)構造函數不依賴於Regex(string, RegexOptions)構造函數,若是不使用Regex,連接器刪除整個RegexCompiler代碼路徑(及其對反射發出的依賴)就變得很簡單。而後更好地利用這一點,確保儘量使用更短的電話。這是一種至關常見的模式,以免這種沒必要要的根源。考慮Environment.GetEnvironmentVariable(字符串)。它曾經呼喚環境。GetEnvironmentVariable(string, EnvironmentVariableTarget)重載,傳入默認的EnvironmentVariableTarget. process。相反,依賴關係被倒置了:Environment.GetEnvironmentVariable(string)重載只包含處理流程用例的邏輯,較長的重載有if (target == EnvironmentVariableTarget.Process)返回GetEnvironmentVariable(name);。這樣,僅僅使用簡單重載的最多見狀況就不會引入處理其餘不太常見目標所需的全部代碼路徑。另外一個例子是dotnet/corefx#0944:對於只寫控制檯而不從控制檯讀取的應用程序,它容許更多的控制檯內部連接。
  • 使用延遲初始化,特別是對於靜態字段,例如dotnet/runtime#37909。若是使用了類型並調用了它的任何靜態方法,則須要保存它的靜態構造函數,由靜態構造函數初始化的任何字段也須要保存。若是這些字段在第一次使用時是延遲初始化的,那麼只有在執行延遲初始化的代碼是可訪問的狀況下才須要保留這些字段。
  • 使用特性開關,例如dotnet/runtime#38129(進一步受益於dotnet/runtime#38828)。在許多狀況下,應用程序可能並不須要全部的特性集,好比日誌或調試支持,但從連接器的角度來看,它看到了正在使用的代碼,所以被迫保留它。然而,連接器可以被告知它應該爲已知屬性使用的替換值,例如,你能夠告訴連接器,當它看到一個返回布爾值的類。對於某些屬性,它應該將其替換爲常量false,這將反過來使它可以刪除由該屬性保護的任何代碼。

Peanut Butter


在.NET Core 3.0性能後,我講過「花生醬」,許多小的改進,單獨不必定就會有巨大的差異,但處理成本,是整個代碼,不然塗抹和修復這些集體能夠產生可測量的變化。和之前的版本同樣,在.NET 5中也有不少這樣受歡迎的改進。這裏有少數:

  • 組裝加載更快。因爲歷史緣由,.NET Core有不少小的實現程序集,而拆分的目的也沒有什麼意義。然而,每個須要加載的附加程序集都會增長開銷。dotnet/runtime#2189dotnet/runtime#31991合併了一堆小程序集,以減小須要加載的數量。
  • 更快的數學。改進了對NaN的檢查,使代碼爲double。IsNan和浮動。更小的代碼和更快。來自@john-h-kdotnet/runtime#35456是一個使用SSE和AMD64 intrinsics可測量地加速數學的好例子。CopySign MathF.CopySign。來自@Marusykdotnet/runtime#34452改進了對Matrix3x2和Matrix4x4的散列代碼生成。
  • 更快的加密。來自@vcsjonesdotnet/runtime#36881在System.Security的不一樣位置使用了優化的BinaryPrimitives來代替開放編碼的等效代碼。來自@VladimirKhvostovdotnet/corefx#39600優化了不受歡迎但仍在使用的加密。CreateFromName方法能夠提升10倍以上的速度。
  • 更快的互操做。dotnet/runtime#36257經過在Linux上避免特定於Windows的「ExactSpelling」檢查和在Windows上將其設置爲true來減小入口點探測(在這裏運行時試圖找到用於P/調用的確切本機函數)。來自@NextTurndotnet/runtime#33020使用sizeof(T)而不是Marshal.SizeOf(Type)/Marshal.SizeOf()在一堆地方,由於前者比後者有更少的開銷。而dotnet/runtime#33967dotnet/runtime#35098dotnet/runtime#39059經過使用更多blittable類型、使用span和ref本地變量、使用sizeof等下降了幾個庫的互操做和封送處理成本。
  • 更快的反射發出。反射發射使開發人員可以在運行時寫出IL,若是你可以以一種佔用更少空間的方式發射相同的指令,你就能夠節省存儲序列所需的託管分配。各類IL操做碼在更常見的狀況下有更短的變體,例如,Ldc_I4能夠用來做爲常量加載任何int值,可是Ldc_I4_S更短,能夠用來加載任何sbyte,而Ldc_I4_1更短,用於加載值1。一些庫利用了這一點,並將它們本身的映射表做爲它們的emit代碼的一部分,以使用最短的相關操做碼;別人不喜歡。dotnet/runtime#35427只是將這樣一個映射移動到ILGenerator自己中,使咱們可以刪除dotnet/runtime庫中的全部自定義實現,並在全部這些庫和其餘庫中自動得到映射的好處。
  • 更快的I/O。來自@bbartels改進的BinaryWriter.Write(字符串)的dotnet/runtime#37705,爲各類常見輸入提供了一個快速路徑。而dotnet/runtime#35978改進了在System.IO內部管理關係的方式。經過使用O(1)而不是O(N)查找進行打包。
  • 處處都是小的分配。例如,dotnet/runtime#35005刪除ByteArrayContent中的內存流分配,dotnet/runtime#36228刪除System.Reflection中的List和底層T[]分配。刪除XmlConverter中的char[]分配。在HttpUtility中刪除一個char[]分配,在ModuleBuilder中刪除幾個可能的char[]分配,在dotnet/runtime#32301刪除一些char[]分配從字符串。拆分使用,dotnet/runtime#32422刪除了一個字符[]分配在AsnFormatter, dotnet/runtime#34551刪除了幾個字符串分配在System.IO。文件系統,dotnet/corefx#41363刪除字符[]分配在JsonCamelCaseNamingPolicy, dotnet/coreclr#25631刪除字符串分配從MethodBase.ToString(), dotnet/corefx#41274刪除一些沒必要要的字符串從CertificatePal。AppendPrivateKeyInfo dotnet/runtime#1155經過跨越從SqlDecimal @Wraith2刪除臨時數組,dotnet/coreclr#26584刪除拳擊之前發生在使用方法像GetHashCode方法在一些元組,dotnet/coreclr#27451刪除幾個分配反映在自定義屬性,dotnet/coreclr#27013刪除一些字符串分配從串連用常量代替一些輸入,並且dotnet/runtime#34774從string.Normalize中刪除了一些臨時的char[]分配。

New Performance-focused APIs


這篇文章強調了在.NET 5上運行的大量現有api會變得更好。此外,.NET 5中有許多新的api,其中一些專一於幫助開發人員編寫更快的代碼(更多的關注於讓開發人員用更少的代碼執行相同的操做,或者支持之前不容易完成的新功能)。如下是一些亮點,包括一些api已經被其餘庫內部使用以下降現有api成本的狀況:

  • Decimal(ReadOnlySpan<int>) / Decimal.TryGetBits / Decimal.GetBits (dotnet/runtime#32155):在之前的版本中添加了不少span-based方法有效地與原語交流,decimal並獲得span-based TryFormat和{}嘗試解析方法,但這些新方法在.NET 5使有效地構建一個十進制從跨度以及提取位decimal跨度。您能夠看到,這種支持已經在SQLDecimal、BigInteger和System.Linq和System.Reflection.Metadata中使用。
  • MemoryExtensions.Sort(dotnet/coreclr#27700)。 我以前談到過:新的Sort 和Sort<TKey,TValue>擴展方法可對任意範圍的數據進行排序。 這些新的公共方法已經在Array自己( dotnet/coreclr#27703)和System.Linq( dotnet/runtime#1888)中使用。
  • GC.AllocateArray 和GC。AllocateUninitializedArray ( dotnet/runtime#33526)。這些新的api就像使用新的T[length],除了有兩個專門的行爲:使用未初始化的變量容許GC交還數組沒有強行清算他們(除非它們包含引用,在這種狀況下,必須明確至少),並經過真實bool固定參數返回重新固定數組對象堆(POH),從該數組在內存中保證永不動搖,這樣他們能夠被傳遞給外部代碼沒有把他們(即不使用固定或GCHandle)。StringBuilder得到支持使用未初始化的特性( dotnet/coreclr#27364)下降成本擴大其內部存儲,同樣新的TranscodingStream ( dotnet/runtime#35145),甚至新的支持從隱私加強進口X509證書和集合郵件證書(PEM)文件( dotnet/runtime#38280)。您還能夠看到在Windows SocketsAsyncEventArgs ( dotnet/runtime#34175)實現中很好地使用了固定支持,其中須要爲諸如ReceiveMessageFrom之類的操做分配固定緩衝區。
  • StringSplitOptions。TrimEntries (dotnet /運行時# 35740)。字符串。分割重載接受一個StringSplitOptions enum,該enum容許分割可選地從結果數組中刪除空條目。新的TrimEntries枚舉值在使用或不使用此選項時首先調整結果。不管是否使用RemoveEmptyEntries,這都容許Split避免爲一旦被修剪就會變成空的條目分配字符串(或者爲分配的字符串更小),而後與RemoveEmptyEntries一塊兒在這種狀況下使結果數組更小。另外,Split的使用者隨後對每一個字符串調用Trim()是很常見的,所以將修剪做爲Split調用的一部分能夠消除調用者額外的字符串分配。這在dotnet/運行時中的一些類型和方法中使用,如經過DataTable、HttpListener和SocketsHttpHandler。
  • BinaryPrimitives。{嘗試}{讀/寫}{雙/單}{大/小}尾數法(dotnet /運行時# 6864)。例如,在。net 5 (dotnet/runtime#34046)中添加的新的簡潔二進制對象表示(CBOR)支持中,您能夠看到使用了這些api。
  • MailAddress。TryCreate (dotnet/runtime#1052 from @MarcoRossignoli)和PhysicalAddress。{}嘗試解析(dotnet 和PhysicalAddress。{}嘗試解析(dotnet ) /運行時# 1057)。新的Try重載支持無異常的解析,而基於跨的重載支持在更大的上下文中解析地址,而不會致使子字符串的分配。
  • unsafeSuppressExecutionContextFlow)(來自@MarcoRossignolidotnet/runtime#706)。 默認狀況下,.NET中的異步操做會流動ExecutionContext,這意味着調用站點在執行繼續代碼時會隱式「捕獲」當前的ExecutionContext並「還原」它。 這就是AsyncLocal 值如何經過異步操做傳播的方式。 這種流一般很便宜,可是仍然有少許開銷。 因爲套接字操做可能對性能相當重要,所以當開發人員知道實例引起的回調中將不須要上下文時,能夠使用SocketAsyncEventArgs構造函數上的此新構造函數。 例如,您能夠在SocketHttpHandler的內部ConnectHelper( dotnet/runtime#1381)中看到此用法。
  • Unsafe.SkipInit<T>  (dotnet/corefx#41995)。c#編譯器明確的賦值規則要求在各類狀況下爲參數和局部變量賦值。在很是特定的狀況下,這可能須要額外的賦值,而不是實際須要的,在計算每條指令和性能敏感代碼中的內存寫入時,這多是不可取的。該方法有效地使代碼僞裝已寫入參數或本地,而實際上並無這樣作。它被用於對Decimal的各類操做(dotnet/runtime#272377),在IntPtr和UIntPtr的一些新的api (dotnet/runtime#307來自@john-h-k),在Matrix4x4 (dotnet/runtime#36323來自@eanova),在Utf8Parser (dotnet/runtime#33507),和在UTF8Encoding (dotnet/runtime#31904)
  • SuppressGCTransitionAttribute (dotnet/coreclr#26458)。這是一個用於P/invoke的高級屬性,它使運行時可以阻止它一般會引起的協做-搶佔模式轉換,就像它在對運行時自己進行內部「FCalls」時所作的那樣。須要很是當心地使用該屬性(請參閱屬性描述中的詳細註釋)。即便如此,你能夠看到在Corelib (dotnet/runtime#27473)中的一些方法使用了它,而且JIT有一些懸而未決的變化,這將使它變得更好(dotnet/runtime#39111)。
  • CollectionsMarshal.AsSpan (dotnet/coreclr# 26867)。這個方法爲調用者提供了對List 的後臺存儲的基於spaner的訪問。
  • MemoryMarshal.GetArrayDataReference (dotnet/runtime#1036)。這個方法返回對數組第一個元素的引用(或者若是數組不是空的,它應該在哪裏)。沒有執行驗證,所以它既危險又很是快。這個方法在Corelib的不少地方被使用,都是用於很是低級的優化。例如,它被用做前面討論的c# (dotnet/runtime#1068)中實現的cast helper的一部分,以及使用緩衝區的一部分。Memmove在不一樣的地方(dotnet/runtime#35733)。
  • SslStreamCertificateContext (dotnet/runtime#38364)。當SslStream.AuthenticateAsServer{Async}提供了使用的證書,它試圖構建完整的X509鏈,一個操做能夠有不一樣數量的相關成本,甚至執行I/O,若是須要下載額外的證書信息。在某些狀況下,用於建立任意數量的SslStream實例的相同證書可能會發生這種狀況,從而致使重複的開銷。SslStreamCertificateContext做爲此類計算結果的一種緩存,工做能夠在advanced中執行一次,而後傳遞給SslStream以實現任意程度的重用。這有助於避免重複的工做,同時也爲呼叫者提供了更多的可預測性和對任何故障的控制。
  • HttpClient。發送(dotnet/runtime#34948)。對於一些讀者來講,看到這裏調用的同步API可能會感到奇怪。雖然HttpClient是爲異步使用而設計的,但咱們發現了開發人員沒法利用異步的狀況,例如在實現僅同步的接口方法時,或者從須要同步響應的本地操做調用時,下載數據的需求無處不在。在這些狀況下,強迫開發人員執行「異步之上的同步」(即執行異步操做,而後阻塞等待它完成)的性能和可伸縮性都不如一開始就使用同步操做。所以,.NET 5看到了添加到HttpClient及其支持類型的有限的新同步表面積。dotnet/runtime自己在一些地方使用了這個。例如,在Linux上,當X509Certificates support須要下載一個證書做爲構建鏈的一部分時,它一般在一個代碼路徑上,這個代碼路徑須要在返回到OpenSSL回調的全部過程當中是同步的;之前,這將使用HttpClient。GetByteArrayAsync,而後阻塞等待它完成,但這被證實給一些用戶形成明顯的可伸縮性問題…dotnet/runtime#38502改變它使用新的同步API代替。相似地,舊的HttpWebRequest類型是創建在HttpClient之上的,在之前的.NET Core版本中,它的同步GetResponse()方法其實是在異步之上進行同步;就像dotnet/runtime#39511同樣,它如今使用同步HttpClient。發送方法。
  • HttpContent.ReadAsStream (dotnet/runtime#37494)。這在邏輯上是HttpClient的一部分。發送上面提到的努力,但我單獨調用它,由於它自己是有用的。現有的ReadAsStreamAsync方法有點奇怪。它最初被公開爲異步,只是爲了防止自定義HttpContent派生類型須要異步,可是幾乎沒有發現任何覆蓋HttpContent的狀況。ReadAsStreamAsync不是同步的,HttpClient請求返回的實現都是同步的。所以,調用方最終爲返回的流的Task 包裝器對象付費,而實際上它老是當即可用的。所以,新的ReadAsStream方法在這種狀況下能夠避免額外的任務 分配。您能夠看到在dotnet/runtime中以這種方式在不一樣的地方使用它,好比ClientWebSocket實現。
  • 非泛型TaskCompletionSource (dotnet/runtime#37452)。因爲引入了Task和Task , TaskCompletionSource 是一種構建任務的方法,調用者能夠經過它的{Try}Set方法手動完成這些任務。並且因爲Task 是從Task派生的,因此單個泛型類型能夠同時用於泛型任務 和非泛型任務需求。然而,這並不老是顯而易見的人,致使混亂對非泛型的狀況下,正確的解決方案加重了歧義的類型時使用T只是信口開河的.NET 5添加了一個非泛型TaskCompletionSource,不只消除了困惑,可是幫助一點性能,由於它避免了任務須要隨身攜帶一個無用的空間T。
  • Task.WhenAny(Task, Task)(dotnet/runtime#34288 dotnet/runtime#37488)。 之前,能夠將任意數量的任務傳遞給Task.WhenAny並經過其重載接受參數Task[] tasks。 可是,在分析此方法的使用時,發現絕大多數呼叫站點始終經過兩項任務。 新的公共重載針對這種狀況進行了優化,關於此重載的一件整潔的事情是,僅從新編譯這些調用站點將使編譯器綁定到新的更快的重載而不是舊的重載,所以無需進行任何代碼更改便可受益於重載。
private Task _incomplete = new TaskCompletionSource<bool>().Task;

[Benchmark]
public Task OneAlreadyCompleted() => Task.WhenAny(Task.CompletedTask, _incomplete);

[Benchmark]
public Task AsyncCompletion()
{
    AsyncTaskMethodBuilder atmb = default;
    Task result = Task.WhenAny(atmb.Task, _incomplete);
    atmb.SetResult();
    return result;
}
Method Runtime Mean Ratio Allocated
OneAlreadyCompleted .NET FW 4.8 125.387 ns 1.00 217 B
OneAlreadyCompleted .NET Core 3.1 89.040 ns 0.71 200 B
OneAlreadyCompleted .NET 5.0 8.391 ns 0.07 72 B
AsyncCompletion .NET FW 4.8 289.042 ns 1.00 257 B
AsyncCompletion .NET Core 3.1 195.879 ns 0.68 240 B
AsyncCompletion .NET 5.0 150.523 ns 0.52 160 B


還有太多System.Runtime.Intrinsics方法甚至開始提到!


New Performance-focused Analyzers


c#「Roslyn」編譯器有一個很是有用的擴展點,稱爲「analyzers」或「Roslyn analyzers」。分析器插入到編譯器中,並被授予對編譯器操做的全部源代碼以及編譯器對代碼的解析和建模的徹底讀訪問權,這使得開發人員可以將他們本身的自定義分析插入到編譯中。最重要的是,分析器不只能夠做爲構建的一部分運行,並且能夠在開發人員編寫代碼時在IDE中運行,這使得分析器可以就開發人員如何改進代碼提供建議、警告和錯誤。分析器開發人員還能夠編寫可在IDE中調用的「修復程序」,並將標記的代碼自動替換爲「修復的」替代品。全部這些組件均可以經過NuGet包分發,這使得開發人員很容易使用其餘人編寫的任意分析。
Roslyn分析程序回購包含一組定製分析程序,包括舊FxCop規則的端口。它還包含新的分析程序,對於.NET5, .NET SDK將自動包含大量這些分析程序,包括爲這個發行版編寫的全新分析程序。這些規則中有多個與性能相關,或者至少部分與性能相關。下面是一些例子:
檢測意外分配,做爲距離索引的一部分。c# 8引入了範圍,這使得對集合進行切片變得很容易,例如someCollection[1..3]。這樣的表達式能夠轉換爲使用集合的索引器來獲取一個範圍,例如public MyCollection this[Range r] {get;},或者若是沒有這樣的索引器,則使用Slice(int start, int length)。根據慣例和設計準則,這樣的索引器和切片方法應該返回它們所定義的相同類型,所以,例如,切片一個T[]將產生另外一個T[],而切片一個Span將產生一個Span。可是,這可能會致使隱式強制轉換隱藏意外的分配。例如,T[]能夠隱式轉換爲Span,但這也意味着T[]切片的結果能夠隱式轉換爲Span,這意味着以下代碼Span Span = _array[1..3];將很好地編譯和運行,除了它將致使由_array[1..]產生的數組片的數組分配。3]索引範圍。更有效的編寫方法是Span Span = _array.AsSpan()[1..3]。這個分析器將檢測幾個這樣的狀況,並提供解決方案來消除分配。

[Benchmark(Baseline = true)]
public ReadOnlySpan<char> Slice1()
{
    ReadOnlySpan<char> span = "hello world"[1..3];
    return span;
}

[Benchmark]
public ReadOnlySpan<char> Slice2()
{
    ReadOnlySpan<char> span = "hello world".AsSpan()[1..3];
    return span;
}
Method Mean Ratio Allocated
Slice1 8.3337 ns 1.00 32 B
Slice2 0.4332 ns 0.05


優先使用流的內存重載。.NET Core 2.1爲流添加了新的重載。ReadAsync和流。分別對Memory和ReadOnlyMemory操做的WriteAsync。這使得這些方法能夠處理來自其餘來源的數據,而不是byte[],而且還能夠進行優化,好比當{ReadOnly}內存是按照指定的方式建立的,它表示已經固定的或不可移動的數據時,能夠避免進行固定。然而,新重載的引入也爲選擇這些方法的返回類型提供了新的機會,咱們分別選擇了ValueTask和ValueTask,而不是Task和Task。這樣作的好處是容許以更同步的方式完成調用來避免分配,甚至以更異步的方式完成調用來避免分配(儘管覆蓋的開發人員須要付出更多的努力)。所以,傾向於使用新的重載而不是舊的重載一般是有益的,這個分析器將檢測舊重載的使用並提供修復程序來自動切換到使用新重載,dotnet/runtime#35941有一些在發現的修復案例的例子。

private NetworkStream _client, _server;
private byte[] _buffer = new byte[10];

[GlobalSetup]
public void Setup()
{
    using Socket listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    var client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
    listener.Bind(new IPEndPoint(IPAddress.Loopback, 0));
    listener.Listen();
    client.Connect(listener.LocalEndPoint);
    _client = new NetworkStream(client);
    _server = new NetworkStream(listener.Accept());
}

[Benchmark(Baseline = true)]
public async Task ReadWrite1()
{
    byte[] buffer = _buffer;
    for (int i = 0; i < 1000; i++)
    {
        await _client.WriteAsync(buffer, 0, buffer.Length);
        await _server.ReadAsync(buffer, 0, buffer.Length); // may not read everything; just for demo purposes
    }
}

[Benchmark]
public async Task ReadWrite2()
{
    byte[] buffer = _buffer;
    for (int i = 0; i < 1000; i++)
    {
        await _client.WriteAsync(buffer);
        await _server.ReadAsync(buffer); // may not read everything; just for demo purposes
    }
}
Method Mean Ratio Allocated
ReadWrite1 7.604 ms 1.00 72001 B
ReadWrite2 7.549 ms 0.99


最好在StringBuilder上使用類型重載。附加和StringBuilder.Insert有許多重載,不只用於追加字符串或對象,還用於追加各類基本類型,好比Int32。即使如此,仍是常常會看到像stringBuilder.Append(intValue.ToString())這樣的代碼。StringBuilder.Append(Int32)重載的效率更高,不須要分配字符串,所以應該首選重載。這個分析儀帶有一個fixer來檢測這種狀況,並自動切換到使用更合適的過載。

[Benchmark(Baseline = true)]
public void Append1()
{
    _builder.Clear();
    for (int i = 0; i < 1000; i++)
        _builder.Append(i.ToString());
}

[Benchmark]
public void Append2()
{
    _builder.Clear();
    for (int i = 0; i < 1000; i++)
        _builder.Append(i);
}
Method Mean Ratio Allocated
Append1 13.546 us 1.00 31680 B
Append2 9.841 us 0.73


首選StringBuilder.Append(char),而不是StringBuilder.Append(string)。將單個字符附加到StringBuilder比附加長度爲1的字符串更有效。可是,像private const string Separator = ":"這樣的代碼仍是很常見的。…;若是const被更改成private const char Separator = ':';會更好。分析器將標記許多這樣的狀況,並幫助修復它們。在dotnet/runtime中針對分析器修正的一些例子在dotnet/runtime#36097中。

[Benchmark(Baseline = true)]
public void Append1()
{
    _builder.Clear();
    for (int i = 0; i < 1000; i++)
        _builder.Append(":");
}

[Benchmark]
public void Append2()
{
    _builder.Clear();
    for (int i = 0; i < 1000; i++)
        _builder.Append(':');
}
Method Mean Ratio
Append1 2.621 us 1.00
Append2 1.968 us 0.75


優先選擇IsEmpty而不是Count。 與前面的LINQ Any() vs Count()類似,某些集合類型同時公開了IsEmpty屬性和Count屬性。 在某些狀況下,例如像ConcurrentQueue 這樣的併發集合,肯定集合中項目數的準確計數比僅肯定集合中是否有任何項目要昂貴得多。 在這種狀況下,若是編寫代碼來執行相似if(collection.Count!= 0)的檢查,則改成使用if(!collection.IsEmpty)會更有效。 該分析儀有助於發現並修復此類狀況。

[Benchmark(Baseline = true)]
public bool IsEmpty1() => _queue.Count == 0;

[Benchmark]
public bool IsEmpty2() => _queue.IsEmpty;
Method Mean Ratio
IsEmpty1 21.621 ns 1.00
IsEmpty2 4.041 ns 0.19


首選Environment.ProcessId。 dotnet/runtime#38908 添加了新的靜態屬性Environment.ProcessId,該屬性返回當前進程的ID。 看到之前嘗試使用Process.GetCurrentProcess()。Id執行相同操做的代碼是很常見的。 可是,後者的效率明顯較低,它沒法輕鬆地支持內部緩存,所以在每次調用時分配一個可終結對象並進行系統調用。 這款新的分析儀有助於自動查找和替換此類用法。

[Benchmark(Baseline = true)]
public int PGCPI() => Process.GetCurrentProcess().Id;

[Benchmark]
public int EPI() => Environment.ProcessId;
Method Mean Ratio Allocated
PGCPI 67.856 ns 1.00 280 B
EPI 3.191 ns 0.05


避免循環中的stackalloc。這個分析器並不能很大程度上幫助您使代碼更快,可是當您使用了使代碼更快的解決方案時,它能夠幫助您使代碼正確。具體來講,它標記使用stackalloc從堆棧分配內存,但在循環中使用它的狀況。從堆棧中分配的內存的一部分stackalloc可能不會被釋放,直到方法返回,若是stackalloc是在一個循環中使用,它可能致使比開發人員分配更多的內存,並最終致使堆棧溢出,崩潰的過程。你能夠在dotnet/runtime#34149中看到一些修復的例子。

What's Next?


根據.NET路線圖,.NET 5計劃在2020年11月發佈,這離咱們還有幾個月的時間。雖然這篇文章展現了大量的性能進步已經釋放,我指望咱們將會看到大量的額外性能改進發如今.NET 5,若是沒有其餘緣由比目前PRs等待一羣(除了前面提到的其餘討論),例如dotnet/runtime#34864dotnet/runtime#32552進一步提升Uri, dotnet/runtime#402 vectorizes string.Compare ,dotnet/runtime#36252改善性能的Dictionary 查找OrdinalIgnoreCase經過擴展示有non-randomization優化不區分大小寫, dotnet/runtime#34633 提供了一個異步執行DNS解析在Linux上, dotnet/runtime#32520顯著減小的開銷Activator.CreateInstance (), dotnet/runtime#32843 Utf8Parser。試着更快地解析Int32值, dotnet/runtime#35654提升了Guid相等度檢查的性能, dotnet/runtime#39117下降了eventlistener處理事件源事件的成本,而 dotnet/runtime#38896@Bond-009特殊狀況下更多的輸入到Task.WhenAny。


最後,雖然咱們真的很努力地避免性能退化,可是任何版本都將不可避免地出現一些性能退化,而且咱們將花費時間調查咱們找到的性能退化。這樣的迴歸與一個已知的類特性使得在.NET5: ICU .NET Framework和之前版本的.NET Core 在Windows上使用國家語言支持(NLS) api全球化在Windows上,而net核心在Unix上使用國際Unicode (ICU).NET 5組件切換到使用默認ICU在全部操做系統若是是可用的(Windows 10包括截至2019年5月更新),使更好的行爲一致性操做系統。可是,因爲這兩種技術具備不一樣的性能概要,所以某些操做(特別是識別區域性的字符串操做)在某些狀況下可能會變得更慢。雖然咱們但願減小其中的大部分(這也將有助於提升Linux和macOS上的性能),可是若是保留下來的任何更改均可能對您的應用程序可有可無,那麼若是這些更改對您的特定應用程序產生了負面影響,您能夠選擇繼續使用NLS。


有了.NET 的預覽和每晚的構建版本,我鼓勵您下載最新的版本,並在您的應用程序中試用它們。若是你發現你認爲能夠和應該改進的東西,咱們歡迎你的PRs到dotnet/runtime!
編碼快樂!

因爲文章較長真的是用了很長時間,中間機翻加糾正了一些地方,不過結局仍是好的最後仍是整理完成。但願能對你們有幫助,謝謝!

image

參考:https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-5/

相關文章
相關標籤/搜索