【微軟學院】性能是.NET Core的一個關鍵特性

【微軟學院】性能是.NET Core的一個關鍵特性

來自專欄 微軟資訊

dotNET跨平台 | Maarten Balliauw

關鍵要點

  • .NET Core是跨平台的,可運行在Windows、Linux、Mac OS X和更多平台上;與.NET相比,發布周期要短得多。大多數.NET Core 都是通過NuGet軟體包交付的,可以很容易地發布和升級。
  • 更快速的發布周期對性能提升工作以及改進諸如SortedSet和LINQ . tolist()方法等語言結構性能的大量工作都有著特別的幫助。
  • 通過引入了System.ValueTuple和Span這樣的類型,更快的周期和更容易的升級也為迭代改進 .NET Core性能的新想法帶來了機會。
  • 這些改進之後可以反饋到完整的 .NET 框架中。

隨著.NET Core2.0的發布,微軟有了下一個主要版本的通用目標,模塊化、跨平台和開源平台最初發佈於2016年。.NET Core 已經創建了許多api,這些api可以在.NET 框架的當前版本中使用。它最初是為下一代ASP.NET創建的解決方案,但現在是驅動、是許多其他場景的基礎,包括物聯網、雲和下一代移動解決方案。在本系列中,我們將探討一些.NET Core的好處,以及它如何不僅能讓傳統的.NET開發人員受益,還能讓所有需要為市場帶來健壯、高性能和經濟解決方案的技術人員受益。

現在,.NET Core正在路上,微軟和開源社區可以在框架的新特性和增強上進行更快速的迭代。在.NET Core中,性能是持續關注的一個領域: .NET Core在執行速度和內存分配方面都帶來了許多優化。

在這篇文章中,我們將討論一些優化,以及如何在以後的性能工作中更多地使用連續流或Span<T>,為我們的開發人員生活帶來幫助。

.NET 和.NET Core

在深入研究之前,讓我們先看看完整的.NET框架(為方便起見我們稱之為.NET)和.NET Core之間的主要區別。為了簡化問題,讓我們假設兩個框架都遵循.NET標準,它本質上是一個規範,定義了所有.NET的基類庫基線。這使得兩個世界非常相似,除了兩個主要的區別:

首先,.NET主要是在Windows上的,而.NET Core是跨平台的,可運行在Windows、Linux、Mac OS X和更多平台上。第二,發布周期非常不同。.NET作為一個完整的框架安裝程序,它是系統範圍的,通常是Windows安裝的一部分,使發布周期更長。對於.NET Core,在一個系統上可以有多個.NET Core安裝,而且沒有長時間的發布周期:大多數.NET Core是以NuGet包交付的,可以輕鬆地發布和升級。

最大的優點是.NET Core世界可以更快地迭代並文學地嘗試新概念,並最終將它們反饋到完整的.NET框架中,作為未來.NET標準的一部分。

經常(但不總是),.NET Core的新特性是由c#語言設計驅動的。因為框架可以更快地進化,語言也可以。一個快速發布周期和性能增強的主要例子是System.ValueTuple。c#和VB.NET 15引入了「值元組」,這很容易添加到.NET Core,因為更快的發布周期,並且針對完整的.NET 4.5.2和更早的版本,可以作為一個NuGet包用於完整的.NET,在.NET 4.7中也可以僅成為完整的.NET框架的一部分。

現在讓我們來看看其中的一些性能和內存改進。

.NET Core的性能改進

.NET Core工作的優點之一是,許多東西要麼需要重新構建,要麼需要從完整的.NET框架中移植。讓所有的內部構件在flux中運行一段時間,再加上快速發布周期,提供了一個在代碼中進行一些性能改進的機會,以前,這些性能改進幾乎被認為是「不要碰,它剛剛正常工作!「。

讓我們從SortedSet和它的Min和Max的實現開始。SortedSet是通過利用自平衡樹結構,以有序順序維護的對象集合。在此之前,從該集合中獲取最小或最大對象需要向下遍歷樹(或向上),調用每個元素的委託,並將返回值設置為當前元素的最小值或最大值,最終到達樹的頂部或底部。調用該委託並傳遞對象意味著有相當多的開銷。直到有一個開發人員看到了這棵樹,並刪除了不需要的委託調用,因為它沒有提供任何值。他自己的基準測試顯示有30%-50%的性能提升。

另一個很好的例子是在LINQ中,在常用的. tolist()方法中更具體。大多數LINQ方法在IEnumerable上作為擴展方法操作,以提供查詢、排序和諸如. tolist()之類的方法。通過這樣做,我們不必關心底層IEnumerable的實現,只要 能夠遍歷它就行了。

缺點是,當調用. tolist()時,我們不知道要創建的列表的大小,只枚舉enumerable中的所有對象,這把即將返回的列表的大小增加了一倍。這有點愚蠢,因為它潛在地浪費了內存(和CPU周期)。因此,如果底層IEnumerable實際上是具有已知大小的列表或數組,那麼就會更改為創建一個已知大小的列表或數組。來自.NET團隊的基準測試顯示,這些數據的吞吐量增加了4倍。

當查看GitHub上CoreFX實驗室存儲庫中的pull請求時,我們可以看到微軟和社區都做出了大量的性能改進。因為.NET Core是開源的,你也可以提供性能修正。其中大多數都是:對.NET中的現有類進行修復。但還有更多:.NET Core還介紹了一些關於性能和內存的新概念,這些概念不僅僅是修復這些現有的類。讓我們來看看本文其餘部分的內容。

減少使用System.ValueTuple的分配

假設我們想從一個方法返回多個值。以前,我們要麼使用out參數,這讓人用起來非常不爽,而且在編寫async方法時也不支持。另一種選擇是使用System.Tuple作為返回類型,但它分配了一個對象,並且具有相當不友好的屬性名稱(Item1, Item2,…)。第三種選擇是使用特定類型或匿名類型,但是在編寫代碼時這種做法會引入開銷,因為我們需要定義類型,而且如果我們需要的是嵌入在該對象中的值,它也會造成不必要的內存分配。

遇到元組返回類型,由System.ValueTuple支持。c# 7和VB.NET 15添加了一個語言特性,可以從一個方法返回多個值。下面是之前和之後的示例:

//之前:private Tuple<string, int> GetNameAndAge(){ return new Tuple<string, int>("Maarten", 33);}//之後:private (string, int) GetNameAndAge(){ return ("Maarten", 33);}

在第一個例子中,我們分配一個元組。因為幾乎不必做什麼額外的工作,分配是在託管堆上完成的,在某個時刻,垃圾回收(GC)將不得不清理它。在第二種情況下,編譯器生成的代碼使用的是ValueTuple類型,它本身就是一個struct,並在堆棧上創建,這使我們能夠訪問我們想要處理的兩個值,同時確保在包含的數據結構上不需要做垃圾回收。

如果我們使用ReSharper的中間語言(IL)查看器查看編譯器在上面示例中生成的代碼,那麼就會很明顯看到這種差異。這裡有兩個方法簽名:

//之前:.method private hidebysig instance class [System.Runtime]System.Tuple`2<string, int32> GetNameAndAge() cil managed { // ...}//之後:.method private hidebysig instance valuetype [System.Runtime]System.ValueTuple`2<string, int32> GetNameAndAge() cil managed { // ...}

我們可以清楚地看到第一個示例返回一個類的實例,第二個例子返回一個值類型的實例。類是在託管堆中分配的(由CLR跟蹤和管理,並受垃圾收集的管制,是可變的),而值類型分配在堆棧上(速度快且較少的開銷,是不可變的)。簡而言之: System.ValueTuple本身並沒有被CLR跟蹤,它只是作為我們關心的嵌入值的一個簡單容器。

請注意,在其優化的內存使用情況下,像元組解構這樣的特性是非常令人愉快的副產品,它使這部分語言和框架都成為了這一部分。

使用Span<T>減少子字元串的內存分配

在前一節中,我們已經討論了棧和託管堆。大多數.NET開發人員只使用託管堆,但.NET有三種類型的內存可供使用,這取決於具體情況:

  • 棧內存——我們通常分配的值類型的內存空間,比如int, double, bool,……它非常快(通常在CPU的緩存中使用),但大小有限(通常小於1 MB)。富有挑戰精神的開發人員會使用stackalloc關鍵字添加自定義對象,但要知道它們是有危險性的,因為在任何時間都可能發生StackOverflowException,使我們的整個應用程序崩潰。
  • 非託管內存——沒有垃圾收集器的內存空間,我們必須自己使用像Marshal.AllocHGlobal 和Marshal.FreeHGlobal之類的方法預訂和釋放內存。
  • 託管內存/託管堆——垃圾收集器釋放已經不再使用的內存空間,使我們大多數人都過著無憂無慮的程序員生活,很少有內存問題。

它們都有各自的優缺點,並有特定的用例。但是,如果我們想要編寫一個與所有這些內存類型兼容的庫該怎麼辦呢? 我們必須分別為他們提供方法。一個針對託管對象,另一個針對指針指向堆棧上或非託管堆上的對象。一個很好的例子就是創建一個字元串的子字元串。我們需要獲取一個System.String並返回一個新System.String的方法,即要處理的託管版本的子字元串。非託管/堆棧版本將使用char*(是的,一個指針!)和字元串的長度,並返回類似的指向結果的指針。難以控制…

這個System.Memory NuGet包(目前仍是預覽版)引入了一個新的Span<T>結構。它是一個值類型(因此沒有被垃圾收集器跟蹤),它試圖統一對任何底層內存類型的訪問。它提供了一些方法,但本質上是這樣的:

  • 一個T的引用
  • 一個可選的開始索引
  • 一個可選的長度

一些實用函數可以抓取一個Span<T>的切片,複製內容,…

把它想成這個(偽代碼):

public struct Span<T>{ ref T _reference; int _length; public ref T this[int index] { get {...} }}

不管我們是使用字元串、char[]甚至是未管理的char*來創建一個Span<T>, Span<T>對象都提供了相同的函數,比如返回索引中的元素。可以把它看作是T[],其中T可以是任何類型的內存。如果我們想要編寫一個子Substring()方法來處理所有類型的內存,那麼我們所要關心的就是正在使用的Span<char>是如何工作的 (或者它的不可變版本,ReadOnlySpan<T>):

ReadOnlySpan<char> Substring(ReadOnlySpan<char> source, int startIndex, int length);

這裡的source 參數可以是基於System.String的span,或者是未管理的char*,我們不需要關心。

而是讓我們暫時忘掉內存類型不可知的方面,並關注性能。如果我們要為System.String編寫一個Substring()方法,我們可能會想到的:

string Substring(string source, int startIndex, int length)

string Substring(string source, int startIndex, int length){ var result = new char[length]; for (var i = 0; i < length; i++) { result[i] = source[startIndex + i]; } return new string(result);}

這很好,但實際上我們正在創建子字元串的副本。如果我們調用Substring(「Hello World!」,0,5),我們在內存中有兩個字元串: 「Hello World」和「Hello」可能會浪費內存空間,我們的代碼仍然需要將數據從一個數組複製到另一個數組,以實現這一點,消耗了CPU周期。我們的實現並不壞,但也不理想。

想像一下一個web框架的實現,它使用上面的代碼從一個包含header和body的HTTP請求中獲取請求體。我們必須分配具有重複數據的大塊內存:一個具有整個傳入請求的內存和一個僅包含請求體的子字元串。然後是需要從原始字元串複製數據到子字元串的開銷。

現在,讓我們用 (ReadOnly)Span<T>來重寫它:

static ReadOnlySpan<char> Substring(ReadOnlySpan<char> source, int startIndex, int length){ return source.Slice(startIndex, length);}

好吧,它變短了,但其實還有更多變化。由於實現了方法Span<T>,所以我們的方法不返回源數據的副本,而是返回引用源的子集的Span<T>。或者在將HTTP請求拆分為header和body的例子中:我們有3個Span:傳入的HTTP請求,指向原始數據的頭部分的一個span<T>,指向請求體的另一個Span<T>。數據在內存中只有一份(創建第一個Span的數據),其他所有的數據只會指向原始數據的切片。沒有重複數據,沒有複製和複製數據的開銷。

結論

隨著.NET Core和更快的發布周期,微軟和.NET Core的開源社區可以在與性能相關的新特性上快速迭代。我們已經看到框架中很多改進現有代碼和構造的工作,比如改進LINQ的. tolist()方法。

更快的周期和更容易的升級也帶來了迭代改進.NET Core性能的新想法的機會,通過引入諸如System.ValueTuple and Span<T>之類的類型,使. net開發人員更自然地使用我們在運行時可用的不同類型的內存,同時避免與它們相關的常見缺陷。

想像一下,如果一些.net基類被重寫為Span<T>實現,諸如字元串UTF解析、加密操作、web解析和其他典型的CPU和內存消耗任務。這將對框架帶來很大的改進,並且所有的. net開發人員都將受益。事實證明,這正是微軟計劃要做的事情! .NET Core的性能前景光明!

關於作者

Maarten Balliauw喜歡構建web和雲應用程序。他的主要興趣是ASP.NET MVC、 c#、Microsoft Azure、 PHP和應用程序性能。他與別人共同創立了MyGet,他還是JetBrains的開發人員。他是微軟Azure的ASP內部人員和MVP。Maarten在不同的國家和國際活動中經常發言,並在比利時組織Azure用戶組活動。在業餘時間,他自己釀造啤酒。Maarten的博客。

隨著.NET Core2.0的發布,微軟有了下一個主要版本的通用目標,模塊化、跨平台和開源平台最初發佈於2016年。.NET Core 已經創建了許多api,這些api可以在.NET 框架的當前版本中使用。它最初是為下一代ASP.NET創建的解決方案,但現在是驅動、是許多其他場景的基礎,包括物聯網、雲和下一代移動解決方案。在本系列中,我們將探討一些.NET Core的好處,以及它如何不僅能讓傳統的.NET開發人員受益,還能讓所有需要為市場帶來健壯、高性能和經濟解決方案的技術人員受益。


關於Microsoft資訊

愛微軟,愛這裡!——Microsoft資訊是 Microsoft 微軟愛好者的集聚地,致力於匯聚Microsoft 微軟技術與產品的最新動態。

微博:@Microsoft資訊

B站:@Microsoft資訊

微信公眾號:Microsoft資訊(ID:MicrosoftNews)

推薦閱讀:

在 ASP.NET Core 已經推出的今天,IIS 會被砍嗎?
說說C#中IList與List區別?
學了C#語言可以從事哪些工作?
WPF繪製圖表時,1ms更新一次數據,界面變得特別卡?

TAG:微軟Microsoft | NETCore | NET |