.NET Core 3中的性能提升(译文)
回顧我們準(zhǔn)備推出.NET Core 2.0的時(shí)候,我寫了一篇博文來介紹.NET已經(jīng)引入的諸多性能優(yōu)化中的一部分,我很喜歡把它們放在一起講述,也收獲了很多正面反饋,因此我又給.NET Core 2.1,一個(gè)同樣高度聚焦于性能的版本,也做了一篇。經(jīng)過上周的構(gòu)建,以及即將到來的.NET Core 3正式版,我很高興又一次有機(jī)會(huì)去介紹她了。
(為方便,以下簡稱.NET Core為DNC)
DNC3要提供的東西堆積如山,從winform和wpf,到單體exe文件,到異步流,到平臺的intrinsics API(譯者注:用于SIMD編程),到HTTP/2,到快速的JSON讀寫,到程序集卸載,到增強(qiáng)的加密,又這樣又那樣的……有很多新功能值得為之慶賀,但對我來說,性能是令我樂于大清早就去工作的主要特性,而且在DNC3中,有一大堆的性能優(yōu)勢。
在這個(gè)帖子里,我們將帶你領(lǐng)略許多已經(jīng)引入DNC的運(yùn)行時(shí)和核心庫的大大小小的提升,讓你的應(yīng)用和服務(wù)更加輕便快速。
安裝
Benchmark.NET已經(jīng)成已經(jīng)成為了為.NET類庫做評估的良好工具,就像我在DNC2.1的博文里所做的那樣,我還會(huì)使用Benchmark.NET去證實(shí)這些性能的提高,通過這個(gè)帖子,我會(huì)以一些獨(dú)立小測試,介紹那些正被討論的特別增強(qiáng)項(xiàng)目,要復(fù)現(xiàn)這些測試,你可以按照如下步驟:
確保安裝DNC3,和DNC2.1做對照;
新建一個(gè)叫BlogPostBenchmarks的文件夾;
在文件夾中運(yùn)行dotnet new console指令;
將BlogPostBenchmarks.csproj的內(nèi)容換成下面代碼:
5.將Program.cs文件的內(nèi)容換成如下代碼:
要執(zhí)行特定的測試,除非另有說明,否則只需要復(fù)制粘貼測試代碼到上面黃色注釋位置,并執(zhí)行dotnet run -c Release -f netcoreapp2.1 --runtimes netcoreapp2.1 netcoreapp3.0 --filter "*Program*"指令,這會(huì)編譯和執(zhí)行2.1和3上兩個(gè)平臺的測試,并且將測試結(jié)果打印到一張表中。
注意事項(xiàng)
在我們開始之前,我們需要注意幾件事:
1.任何涉及到微測試的結(jié)果討論都有一個(gè)前提,測量結(jié)果在不同的機(jī)器上是不同的,我已經(jīng)盡力嘗試來挑選一些穩(wěn)定的例子來分享(在多臺機(jī)器上以多個(gè)配置運(yùn)行,以確認(rèn)這一測試是有效的)。但是如果你測出來的數(shù)據(jù)不同于我展示出來的,也別太吃驚,我們?nèi)匀豢梢宰C明這些性能提高的重要性,所有的測試結(jié)果來自尚未發(fā)布的DNC 3pre6,這里是我的Windows和Linux配置項(xiàng),作為使用Benchmark.NET的一個(gè)總結(jié):
2.除非另有說明,否則測試都運(yùn)行在Windows上,在很多狀況下,性能在Windows和unix上是等同的,但是在其他平臺上,就會(huì)有不小的差異,在那些.NET依賴于系統(tǒng)功能的地方,而且系統(tǒng)本身有不同的性能表現(xiàn)。
3.我提到了DNC2和2.1,但是沒提DNC2.2,2.2主要聚焦于ASP.NET,在ASP.NET層面有巨大的性能提升,這一版本主要關(guān)注運(yùn)行時(shí)和核心庫提供的服務(wù),大多數(shù)提升在2.1的帖子里就有提及,因此跳過2.2,直接說3.
基于以上前提,讓我們來找點(diǎn)樂子。
Span和它的同類
DNC2.1引入的一個(gè)較重要的特性是Span<T>,還有它的同類ReadOnlySpan<T>,Memory<T>,和ReadOnlyMemory<T>。這些新值類型的引入帶來了上百個(gè)和它們交互的新方法,一些方法在新類型里面,一些覆寫了已有類的方法,以及JIT編譯器里的優(yōu)化讓它們的工作效率大有提高。這一版本也包含了一些Span<T>的內(nèi)部使用(不對外暴露),讓已有的操作更簡潔快速,但依舊保持了可維護(hù)性和安全性。在DNC3中,我們投入諸多附加工作,以提高這些方面的性能:讓運(yùn)行時(shí)更好地為它們(指Span<T>等)生成代碼;在運(yùn)行時(shí)內(nèi)部更多地使用它們來提高其他的操作性能;并且增強(qiáng)和它們交互的不同類庫性能。
要使用Span工作,應(yīng)該首先拿到一個(gè)Span,已經(jīng)有幾個(gè)PR讓這一進(jìn)程加快了(原文and several PRs have made doing so faster)。例如,傳遞一個(gè)Memory<T>然后用它獲得一個(gè)Span<T>是一種獲得Span的常見方法;Stream.WriteAsync和ReadAsync工作的原理是接受一個(gè)ReadOnlyMemory<T>(這樣ReadOnlyMemory<T>就可以放在堆上),當(dāng)實(shí)際的字節(jié)要被讀寫時(shí)進(jìn)入Memory的Span屬性。這個(gè)PR移除了一個(gè)參數(shù)判斷分支以提高Span和Memory的性能(包括ReadOnlyMemory<T>.Span方法和ReadOnlySpan<T>.Slice方法),雖然移除一個(gè)判斷分支是個(gè)小事,但是在一堆有著巨量Span的代碼中(例如格式化和解析),小優(yōu)化就可以聚沙成塔。
更有影響力的是這個(gè)PR,在運(yùn)行時(shí)級別上搞了些奇技淫巧來安全的去除一些運(yùn)行時(shí)類型轉(zhuǎn)換檢查,而且應(yīng)用了位掩碼邏輯(bit masking logic)來允許ReadOnlyMemory<T>去包裝不同的類型,像string,T[](泛型數(shù)組),和MemoryManager<T>,給這些類型提供了一個(gè)無縫的結(jié)合。這些PR的結(jié)果就是很好的加速了從Memory<T>中捕獲Span<T>的性能,也提高了所有依賴于這一機(jī)制的操作的性能。
當(dāng)然,你拿到一個(gè)Span之后肯定是要用它的,這個(gè)類型有無數(shù)種用途,其中很多在DNC3中得到了進(jìn)一步的優(yōu)化。
例如數(shù)組在通過P/Invoke從Span傳遞數(shù)據(jù)到本地(native)代碼時(shí),數(shù)據(jù)必須被固定(除非它已經(jīng)不可移動(dòng)了,例如當(dāng)Span創(chuàng)建時(shí)就用來包裝一些本地分配內(nèi)存或者棧上數(shù)據(jù),而不是GC堆)。要固定一個(gè)Span,最簡單的方式就是依靠C#7.3中加入的模式,來將fixed關(guān)鍵字應(yīng)用于任何Span類型。一個(gè)類型要做的是暴露一個(gè)GetPinnableReference方法(或者擴(kuò)展方法)來返回一個(gè)ref T并傳遞到實(shí)例中存儲的數(shù)據(jù),這樣它就可以用fixed了。
ReadOnlySpan<T>準(zhǔn)確地做到了這一點(diǎn),但是即使ReadOnlySpan<T>.GetPinnableReference已經(jīng)被內(nèi)聯(lián),一個(gè)內(nèi)部調(diào)用的Unsafe.AsRef會(huì)阻止內(nèi)聯(lián),這個(gè)PR修復(fù)了這個(gè)問題,允許整個(gè)操作被內(nèi)聯(lián)。上述代碼進(jìn)而在這個(gè)PR中被魔改,來清除熱點(diǎn)代碼的判斷分支,兩者加在一起引發(fā)了一個(gè)可觀的加速了Span的固定:
這一點(diǎn)值得注意,如果你對這種微優(yōu)化感興趣,你可能避免使用默認(rèn)的固定,至少是在熱點(diǎn)處避免。ReadOnlySpan<T>.GetPinnableReference方法是給數(shù)組和字符串的固定而設(shè)計(jì)的,null或者空輸入只會(huì)導(dǎo)致一個(gè)空指針,這一行為需要進(jìn)行Span長度為0的非空檢查。
如果你的代碼有構(gòu)造器,確保你的Span不會(huì)是空值,你可以選擇使用MemoryMarshal.GetReference,性能相同,但沒有長度檢查:
再一點(diǎn),一個(gè)檢查會(huì)增加少量的開銷,當(dāng)復(fù)讀機(jī)般的執(zhí)行之后,開銷就會(huì)積羽沉舟:
當(dāng)然,有很多其他(也更受人歡迎)的方式去操作Span的數(shù)據(jù),而不是用fixed關(guān)鍵字。比如,讓人有點(diǎn)吃驚的是,直到Span<T>到來之前,.NET都沒有一個(gè)內(nèi)建的memcmp(memory compare,C/C++專屬)等效品,然而Span<T>的SequenceEqual 和SequenceCompareTo方法已經(jīng)成為了.NET比較內(nèi)存區(qū)域數(shù)據(jù)的必經(jīng)之路。在DNC2.1中,這兩個(gè)方法運(yùn)用了System.Numerics.Vector優(yōu)化以實(shí)現(xiàn)向量化,但是SequenceEqual的情況使得它更容易被人利用。在這個(gè)PR中,benaadams針對AVX2和SSE2(兩個(gè)最常見的SIMD指令集)更新了SequenceCompareTo以利用DNC3中新的intrinsic API,導(dǎo)致了比較無論大小的Span的性能的顯著提升。(如果想查閱更多關(guān)于DNC3中intrinsic的信息,可以看這里和那里)。
在后臺,“向量化”是單核心單指令并行執(zhí)行多個(gè)操作的方法,一些優(yōu)化過的編譯器能自動(dòng)向量化(譯者注:例如使用LLVM做后端的Mono AOT,這一點(diǎn)目前的CoreCLR都沒做到),借此編譯器會(huì)分析循環(huán)來判斷是否可以利用指令來生成等效的代碼讓它跑得更快。.NET的JIT編譯器當(dāng)前還不會(huì)自動(dòng)向量化,但手動(dòng)向量化循環(huán)是可能的,相應(yīng)選項(xiàng)在DNC3中性能大為提高,舉個(gè)例子向量化會(huì)長啥樣,想象一下你想搜索一個(gè)byte數(shù)組的第一個(gè)非0 byte值,返回其位置,簡單的方法是迭代所有bytes的位置:
當(dāng)然,對很小的數(shù)組而言它還能有效工作,但是數(shù)組大了之后,這么做就會(huì)多出很多無用功,考慮到64位處理器會(huì)把字節(jié)數(shù)組重譯為long數(shù)組,Span<T>對此有很好的支持。我們可以一次比較8個(gè)字節(jié)而不是一個(gè),以增加代碼復(fù)雜性為代價(jià):只要我們找到一個(gè)非0的long值,我們就可以查看它攜帶的每一個(gè)byte,去找到第一個(gè)非0字節(jié)(雖然還有方法來改進(jìn)這個(gè)操作)。類似的,數(shù)組的長度可能不會(huì)正好是8的倍數(shù),所以我們需要處理溢出。
對于更長的數(shù)組而言,LoopLongs方法有了9倍多的提升我在這里掩蓋了一些細(xì)節(jié),但還是應(yīng)該傳達(dá)核心理念。.NET添加了額外向量化機(jī)制,尤其是上述的System.Numerics.Vector類型允許開發(fā)者使用Vector編寫代碼,然后使用JIT編譯器將其編譯成當(dāng)前平臺上最好的指令。
LoopVectors方法又把性能提高了40%DNC3進(jìn)一步擁有了新的intrinsic api,允許有興趣的開發(fā)者在受支持的硬件上發(fā)揮出最好的性能,利用AVX或SSE這樣的指令集你可以一次比較超過8個(gè)字節(jié),DNC3中的諸多提升來自這些技術(shù)的使用。
回到我們的例子中,復(fù)制Span的性能也有所提升,感謝banaadams提供的這個(gè)PR和那個(gè)PR,尤其是針對小的Span…
搜索在任何程序中,都是最經(jīng)常使用的操作之一,Span的搜索一般使用IndexOf方法,它的變種IndexOfAny和Contains在benaadams的這個(gè)PR中再一次被向量化,這一次提升了IndexOfAny操作字節(jié)時(shí)的性能,字節(jié)在網(wǎng)絡(luò)相關(guān)的解決方案里尤為普遍(例如在線下把字節(jié)解析為HTTP棧的一部分),你可以在下面的測試中看到效果:
我挺喜歡這種優(yōu)化,因?yàn)樗鼈冏銐虻讓右灾劣谒鼈冊诖罅看a調(diào)用的情況下,事半功倍。以上的操作只影響到了字節(jié),但是隨后的PR也覆蓋到了char的優(yōu)化,這個(gè)PR做出了不錯(cuò)的改變,將同樣的變化帶到了同樣規(guī)模的其他主數(shù)據(jù)類型中,例如我們可以將上個(gè)測試重新用在sbyte上,看到這個(gè)PR影響下,一個(gè)類似的性能提高:
另一個(gè)例子,看下這個(gè)PR,這一變化和剛才講到的類似,使用向量化去提升ToUpper/ToLower變種的性能。
這個(gè)PR優(yōu)化了ReadOnlySpan<char>.TrimStart/TrimEnd()的性能,也是個(gè)很普遍使用的方法,得到了可喜的結(jié)果(很難看到結(jié)果中的空白部分,但是表中的結(jié)果是按照Params屬性里的參數(shù)順序排列的)
有時(shí)候優(yōu)化器僅僅在代碼管理上更聰明了些。這個(gè)PR移除了函數(shù)中一個(gè)不必要的卻在很多事關(guān)全局的代碼中起作用的層,僅僅移除那些多余的方法調(diào)用就引起了可觀的加速,例如在小Span情況下……
當(dāng)然,Span最厲害的一點(diǎn)在于,它是可復(fù)用的結(jié)構(gòu)單元,允許很多高級操作,包括在數(shù)組和字符串上……
數(shù)組和字符串
性能優(yōu)化作為DNC的一個(gè)主題,新功能不管在哪,不僅應(yīng)該暴露給大眾使用,內(nèi)部也要用上。畢竟考慮到DNC功能涉及到的深度和廣度,如果關(guān)注性能的特性連DNC本身都不能滿足的話,它多半也不會(huì)滿足客戶的需求。嚴(yán)格來說,在內(nèi)部用上新特性才能證明我們的設(shè)計(jì)合格,評估它們的時(shí)候,很多額外的代碼提供了助益,這些優(yōu)化有了加倍的效果。
這部分不僅僅和新的API有關(guān),在C#7.2和7.3中介紹的很多語法,包括C#8本身都受到了DNC自身需求的影響,并用于優(yōu)化那些我們以前難以優(yōu)化的地方(而不是淪落到去使用非托管的代碼,我們盡力避免去用的那玩意)。例如,這個(gè)PR通過利用C# 7.2的ref locals和7.3的ref local reassignment特性加速了Array.Reserve方法,使用新特性可以讓代碼更好地讓JIT為內(nèi)部循環(huán)生成代碼,然后就是一個(gè)肉眼可見的加速:
數(shù)組還有個(gè)例子,Clear方法在這個(gè)PR里也被優(yōu)化了,處理了讓該方法依賴的隱式memset操作變慢了2倍的對齊問題。這個(gè)改變會(huì)一個(gè)個(gè)地手動(dòng)清理最多幾個(gè)字節(jié),這樣我們就可以把指針交給memset去對齊,如果你“足夠幸運(yùn)”,數(shù)組剛好對齊,性能就不錯(cuò),如果沒有對齊,就會(huì)對性能有不一般的影響,這個(gè)測試模擬了不好的情況:
也就是說,很多性能優(yōu)化其實(shí)是建立在新的API上的,Span就是個(gè)好例子,它在DNC2.1中被引入,初衷是想讓它能用并暴露出足夠多的API讓它能有意義,但同時(shí)我們開始在內(nèi)部使用它,一是檢查我們的設(shè)計(jì),二是利用它帶來的優(yōu)化。這些工作一部分在DNC2.1中完成了,但是影響在DNC3中依然持續(xù),數(shù)組和字串都是這種優(yōu)化的主要受益者。
很多用在Span上的向量化優(yōu)化也一樣地用在了數(shù)組上。benaadams(怎么又是這個(gè)人)的這個(gè)PR針對字節(jié)和char都優(yōu)化了Array.LastIndexOf和IndexOf方法,使用了和Span類內(nèi)部一樣的內(nèi)部輔助方法,也得到了相似的優(yōu)化結(jié)果:
和Span一樣,感謝dschinde的這個(gè)PR,IndexOf的優(yōu)化現(xiàn)在可以應(yīng)用于相同大小的其他基元類型。
向量化優(yōu)化也用在了string上,你可以看優(yōu)化帶師benaadams的這個(gè)PR帶來的效果:
注意一下,DNC2.1因?yàn)閷⒆址麛?shù)組轉(zhuǎn)化為string有額外的分配,但是DNC3就沒了,感謝benaadams的這個(gè)PR。
當(dāng)然有些功能更偏向于string(雖然也能用于Span暴露出來的新函數(shù)),例如用多種字串比較方法計(jì)算哈希值,例如這個(gè)PR提高了執(zhí)行OrdinalIgnoreCase時(shí)String.GetHashCode的性能(OrdinalIgnoreCase和Ordinal(默認(rèn))是兩個(gè)最常使用的模式)。
OrdinalsIgnoreCase也為別的用途優(yōu)化了。例如,這個(gè)PR通過向量化和移除判斷分支,用StringComparer.OrdinalIgnoreCase優(yōu)化了String.Equals的性能(一次檢查兩個(gè)字符而不是一個(gè),并從內(nèi)部循環(huán)中移除了判斷分支:
剛才的情況是String實(shí)現(xiàn)的功能示例,但是還有很多附加的string相關(guān)功能也被優(yōu)化了,例如Char的不少操作性能都得到提升,例如這個(gè)PR和那個(gè)PR改進(jìn)的Char.GetUnicodeCategory:
那些PR還強(qiáng)調(diào)了另一個(gè)從語言改進(jìn)中受益的例子,在C#7.3中,C#編譯器能夠優(yōu)化這個(gè)形式的屬性:
相對于照章編譯,每次調(diào)用都會(huì)分配一個(gè)新的字節(jié)數(shù)組而言,編譯器利用了兩個(gè)特征:a)數(shù)組背后的字節(jié)都是常量,b)返回了一個(gè)ReadOnlySpan,也就是說用戶不能用托管代碼去修改這個(gè)span的數(shù)據(jù)。通過這個(gè)PR,C#編譯器取締了將字節(jié)寫成二進(jìn)制大對象放進(jìn)元數(shù)據(jù)的做法,這個(gè)屬性將只會(huì)生成一個(gè)Span直接指向相應(yīng)數(shù)據(jù),這樣訪問數(shù)據(jù)就會(huì)極快,甚至比返回一個(gè)靜態(tài)字節(jié)數(shù)組還快:
另一個(gè)值得關(guān)注的字串相關(guān)領(lǐng)域是StringBuilder(不僅僅是它自己的優(yōu)化,雖然這個(gè)類的確收到過一些,例如Wraith2在這個(gè)PR中的一個(gè)重載,避免了意外的裝箱并且從一個(gè)ReadOnlyMemory<char>中創(chuàng)建了一個(gè)string添加到構(gòu)建器中)。在很多情況下,StringBuilder用著都是方便起見,但是也增加了消耗,只需要一點(diǎn)小小的工作(以及某些情況下,用到DNC2.1中新的String.Create方法),我們就能消除這些開銷,不管在CPU上還是在內(nèi)存分配上,例子如下……
這個(gè)PR移除了Dns.GetHostEntry方法中的marshal操作使用的StringBuilder:
這個(gè)PR從希伯來語數(shù)字格式化中移除了一個(gè)StringBuilder:
這個(gè)PR從物理地址格式化中移除了一個(gè)StringBuilder:
這個(gè)PR從X509Certificate類的若干屬性中移除了StringBuilder:
諸如此類。
這些PR證明了即使是小小的改動(dòng)也可以大有收獲,讓已有代碼開銷更低并有效地?cái)U(kuò)展到StringBuilder之外。在DNC里面有很多地方用到了String.Substring,其中大部分可以用AsSpan和Slice替代,例如juliushardt的這個(gè)PR,或者那個(gè)PR和29539號PR,以及29227號PR,29721號PR,都從FileSystemWatcher中移除了字符串分配,延遲了這類字串的創(chuàng)建,只在需要的時(shí)候才初始化。
另一個(gè)使用新API去改進(jìn)已有功能的例子是String.Concat。DNC3有幾個(gè)新的String.Concat重載,一個(gè)接收ReadOnlySpan<char>代替string,這樣就很容易避免在連接其他字串的片段時(shí)帶來的子串分配和復(fù)制:我們使用了String.AsSpan和Slice來替代String.Concat和String.Substring。實(shí)際上,給這些新重載提供實(shí)現(xiàn),暴露和添加測試的這個(gè)PR和那個(gè)PR也給DNC添加了幾十個(gè)調(diào)用點(diǎn)(call sites)。這有個(gè)例子,優(yōu)化了Uri.DnsSafe的訪問:
還有個(gè)蠣子,使用Path.ChargeExtension把一個(gè)非空擴(kuò)展名(extension)換成另一個(gè):
最后,一個(gè)非常接近的領(lǐng)域是編碼。關(guān)于Encoding,一大波優(yōu)化已經(jīng)在DNC3中實(shí)現(xiàn),不管是在通用還是特定的編碼中。例如這個(gè)PR允許Encoding.Unicode.GetString在許多地方應(yīng)用一個(gè)已有的極端條件優(yōu)化,又或者是這個(gè)PR從多個(gè)編碼實(shí)現(xiàn)中移除了一堆無用的虛擬間接尋址(其實(shí)就是移除了一些參數(shù)并加上一些sealed),還有這個(gè)PR,通過利用早些時(shí)候提到的“共同元數(shù)據(jù)-二進(jìn)制大對象span“支持,來優(yōu)化Encoding.Preamable;以及這個(gè)PR和那個(gè)PR大改并流水線化了UF8Encoding和AsciiEncoding的實(shí)現(xiàn)。
這些例子都在強(qiáng)調(diào)字串本身或者應(yīng)用于其周邊的改進(jìn),都很不錯(cuò),但是字串相關(guān)的改進(jìn)真正影響的地方是接下來要說到的格式化和解析。
格式化/解析
解析和格式化是任何現(xiàn)代web應(yīng)用或服務(wù)的命脈:線上提取數(shù)據(jù),解析,操作,重新格式化。在DNC2.1中,伴隨著Span<T>的成熟,我們致力于實(shí)現(xiàn)基元類型的格式化和解析,例如從Int32到DateTime。很多這一類型的改動(dòng)都能在我過去的博文中讀到,但是能實(shí)現(xiàn)那些優(yōu)化的重要原因是把很多本機(jī)代碼遷移到托管代碼,這可能有些反直覺,畢竟C代碼比C#代碼更快是“常識”。但是除了它們(指C和C#)之間的鴻溝在縮小以外,(絕大多數(shù))安全的C#代碼更容易去進(jìn)行調(diào)測,所以雖然我們修改那些本機(jī)(native)代碼的實(shí)現(xiàn)看上去反復(fù)無常,但是一般公眾已經(jīng)憑借于此,在一切能優(yōu)化的地方深入優(yōu)化。DNC3中我們?nèi)栽谌^續(xù)這些努力,也得到了超棒的激勵(lì)。
讓我們從核心的Integer基元類型開始吧。這個(gè)PR為整型風(fēng)格的數(shù)據(jù)(如Int32或Int64)添加了一個(gè)特殊的變種,這個(gè)PR為無符號整型添加一個(gè)類似的支持,而這個(gè)PR給16進(jìn)制數(shù)添加了一個(gè)差不多的,除此之外,這個(gè)PR分布在更多的優(yōu)化中,例如將這些改動(dòng)利用在byte一類的基元類型中,跳過無關(guān)緊要的函數(shù)分層,將一些方法調(diào)用流水線化便于內(nèi)聯(lián),進(jìn)一步減少了判斷分支。最終這一版本中解析整型基元類型的性能得到了重大提升。
這些類型的格式化也有所改進(jìn),盡管在DNC2-2.1之間它們已經(jīng)被大幅優(yōu)化。這個(gè)PR修改了代碼結(jié)構(gòu),以避免在不需要的時(shí)候訪問當(dāng)?shù)財(cái)?shù)字格式(例如當(dāng)將一個(gè)值格式化為16進(jìn)制,這個(gè)操作不需要遵守當(dāng)?shù)氐奈幕?#xff0c;又何必去訪問區(qū)域設(shè)置呢?),這個(gè)PR則優(yōu)化了金融數(shù)字格式化的性能,很大程度上是靠優(yōu)化數(shù)據(jù)的傳遞方式(抑或是根本沒傳遞)。
實(shí)際上,DNC3中,System.Decimal自己都被大修了,在這個(gè)PR之后,它就是個(gè)完全托管代碼實(shí)現(xiàn)了,還有一些別的性能工作在這個(gè)PR里面。
回到解析和格式化上,甚至還有一些新的特殊情況的格式化,一開始可能看上去像蔡徐坤,但是代表了實(shí)事求是的優(yōu)化風(fēng)格,在一些大型web應(yīng)用中,我們發(fā)現(xiàn)了托管堆上大量的字串僅僅是由0和1組成的,既然“最快的代碼就是不去執(zhí)行的代碼”,那么為什么要在能緩存和復(fù)用結(jié)果的情況下,一遍遍地分配和格式化這些小數(shù)呢(其實(shí)就是實(shí)現(xiàn)一個(gè)自己的字串拘留池)?這就是這個(gè)PR所做的,給0-9新建一個(gè)特定的字串小緩存,不管我們在什么時(shí)候格式化一個(gè)單數(shù)字整型,只需要從緩存中拉取這些字串。
(有一定時(shí)間效果,同時(shí)避免了空間分配)枚舉類型在DNC3中也得到了很大的解析和格式化性能改進(jìn),這個(gè)PR優(yōu)化了Enum.Parse和Enum.TryParse的處理,不管是泛型還是非泛型的。這個(gè)PR優(yōu)化了[Flags]枚舉的ToString方法,而那個(gè)PR進(jìn)一步提升了其他ToString方法。最終Enum相關(guān)的性能提升也很大:
在DNC2.1中,DateTime.TryFormat和ToString方法已經(jīng)針對通常使用的“o”或“r”格式優(yōu)化過,在DNC3中,等價(jià)的解析也得到了類似處理。這個(gè)PR大大提高了DateTime和DateTimeOffSet的往返“o”格式解析性能,而這個(gè)PR為RFC1123格式做了一樣的事,對任何DateTime的沉重序列化格式,這些改進(jìn)都能弄個(gè)大新聞:
說回剛才說過的StringBuilder,默認(rèn)的DateTime格式化也被這個(gè)PR優(yōu)化了,修改了DateTime和StringBuilder的內(nèi)部交互機(jī)制,用于建立結(jié)果狀態(tài)。
TimeSpan格式化也大有提升,通過這個(gè)PR:
Guid類的解析在這場“優(yōu)化的游戲”中也開始狂舞,通過這個(gè)優(yōu)化它的PR,主要是通過避免輔助線程的開支,還有規(guī)避一些搜索,它們用來決定應(yīng)用哪些線程去解析。
和這有關(guān)的是,這個(gè)PR再一次利用了向量化,優(yōu)化了Guid和byte數(shù)組以及Span之間的互相解析與構(gòu)建。
正則表達(dá)式
正則表達(dá)式經(jīng)常和解析扯到一塊。DNC3中我們對System.Text.RegularExpressions做了點(diǎn)微小的工作。這個(gè)PR用基于ref struct的構(gòu)建器取代了內(nèi)部的StringBuilder緩存,這樣就能利用棧分配的空間和池化的緩存。這個(gè)PR通過進(jìn)一步利用了Span延續(xù)了這一工作,但是Alois-xx的這個(gè)PR帶來了最大的改進(jìn),修改了RegexOptions.CompiledRegex生成的代碼,以避免因?yàn)楫?dāng)前地域帶來無謂的thread-local訪問。當(dāng)用上RegexOptions.IgnoreCase時(shí),這一優(yōu)化更具威力。為了看到實(shí)際影響,我找了一個(gè)Compiled和IgnoreCase都用過的復(fù)雜正則,并做了個(gè)測試:
長到喪心病狂線程
線程是個(gè)一直存在但是大多數(shù)應(yīng)用和庫在大多數(shù)情況下都不需要顯式與其交互的東西,這使得運(yùn)行時(shí)優(yōu)化以盡可能減少開支越發(fā)成熟,這樣用戶代碼就更快了。上一個(gè)DNC版本展示了我們在這一領(lǐng)域投入的努力,DNC3延續(xù)了這個(gè)動(dòng)向。這也是另一個(gè)新的API(指Span)得以暴露并作用于DNC自身的示例。
例如,以前能排進(jìn)ThreadPool隊(duì)列的東西(特指原文的work item,一個(gè)回調(diào))只有那些運(yùn)行時(shí)自帶的,也就是ThreadPool.QueueUserWorkItem和它的同類如Task和Timer創(chuàng)建的任務(wù)。但是在DNC3中,ThreadPool有了一個(gè)UnsafeQueueUserWorkItem方法重載,可以接受新的IThreadPoolWorkItem接口,這個(gè)接口非常簡單,只有一個(gè)Execute方法,任何實(shí)現(xiàn)了這個(gè)接口的對象都可以直接排進(jìn)線程池隊(duì)列。這是高級的用法,大多數(shù)代碼用已有的回調(diào)就可以了。但是更多的選項(xiàng)提供了很多靈活性,尤其是在一個(gè)可重用的對象上實(shí)現(xiàn)這個(gè)接口,這樣它就能反復(fù)排進(jìn)線程池,這一改進(jìn)現(xiàn)在用在DNC3中的許多地方。
System.Threading.Channels中就有一個(gè)這樣的例子,Channels類庫在DNC2.1中引入,已經(jīng)有了一個(gè)很低的配置要求(原文是profile),但是仍然會(huì)有一些時(shí)候它會(huì)分配。例如,創(chuàng)建一個(gè)channel的一個(gè)選項(xiàng)是類庫創(chuàng)建的延續(xù)任務(wù)是否應(yīng)該同步運(yùn)行/異步運(yùn)行,作為任務(wù)完成的一部分(例如當(dāng)一個(gè)Channel上的TryWrite()調(diào)用,喚醒了相應(yīng)的ReadAsync方法,是否ReadAsync的延續(xù)任務(wù)會(huì)被同步調(diào)用,或者被TryWrite調(diào)用排入隊(duì)列)。默認(rèn)情況下延續(xù)任務(wù)從不同步調(diào)用,但是也需要分配一個(gè)對象作為將延續(xù)任務(wù)排列到隊(duì)列的一部分。在這個(gè)PR中,實(shí)現(xiàn)了IThreadPoolWorkItem的可重用IValueTaskSource備份了從ReadAsync返回的ValueTask,因此本身可以排進(jìn)隊(duì)列,避免了分配,起到了很好的優(yōu)化作用。
IThreadPoolWorkItem現(xiàn)在也能用在別的地方,例如ConcurrentExclusiveSchedulerPair(一個(gè)沒多少人知道但有用的類型,提供了一個(gè)限制一次只能執(zhí)行一個(gè)任務(wù)的排他調(diào)度器,一個(gè)一次執(zhí)行用戶指定數(shù)量的任務(wù)的并行調(diào)度器,互相配合使得排他任務(wù)運(yùn)行時(shí),沒有并行任務(wù)在運(yùn)行,就是一個(gè)讀寫鎖)現(xiàn)在也實(shí)現(xiàn)了IThreadPoolWorkItem,這樣就避免了將其排入隊(duì)列時(shí)的分配。這玩意也在ASP.NET?Core中用到,也是ASP.NET測評中每個(gè)請求(request)做到0分配的關(guān)鍵原因之一。但是到目前為止,最具影響力的實(shí)現(xiàn)是async/await的基礎(chǔ)建設(shè)。
在DNC2.1中,運(yùn)行時(shí)對async/await的支持大修過,徹底地減少了涉及到異步方法的分配,以前當(dāng)一個(gè)異步方法第一次等待一個(gè)尚未完成的可等待操作時(shí),基于結(jié)構(gòu)體的狀態(tài)機(jī)將會(huì)被裝箱(就是運(yùn)行時(shí)裝箱)放到堆上。但是在DNC2.1中,我們使用了一個(gè)泛型對象,結(jié)構(gòu)體作為這個(gè)對象的字段而存在。這樣有諸多好處,其中之一是允許在這個(gè)對象上實(shí)現(xiàn)額外的接口,例如IThreadPoolWorkItem。這個(gè)PR很好的做到了這一點(diǎn),并且使得另一個(gè)大范圍應(yīng)用場景進(jìn)一步減少了分配,尤其是用于TaskCompletionSource<T>的TaskCreationOptions.RunContinuationsAsynchronously。可以在下面的測試中看到效果:
Gen0指第0代GC,相當(dāng)于Java的Eden,可以看到我們少產(chǎn)生了很多垃圾這一改動(dòng)帶來了接下來的優(yōu)化,例如這個(gè)PR使用該優(yōu)化進(jìn)行await Task.Yield();無分配:
它還進(jìn)一步用在了Task自己身上,有個(gè)有趣的競態(tài)條件要在等待操作中處理:如果等待之后的操作在調(diào)用IsCompleted之后,卻在OnCompleted之前完成會(huì)發(fā)生什么?提醒一下,看這段代碼:
當(dāng)我們執(zhí)行到IsCompleted返回false的時(shí)候,將調(diào)用AwaitedOnComplated方法然后返回。如果等待操作在調(diào)用AwaitOnCompleted時(shí)完成,我們(又)不想同步調(diào)用重入狀態(tài)機(jī)的延續(xù),因?yàn)槲覀儠?huì)在棧中進(jìn)一步操作,如果這種事情反復(fù)發(fā)生,就會(huì)發(fā)生“潛棧(stack dive)現(xiàn)象”,然后爆棧。相反的是,我們強(qiáng)制排列這個(gè)延續(xù)。這種情況并不普遍,但是會(huì)比你期望的更加頻繁,這只需要一個(gè)快速異步完成的操作(多種網(wǎng)絡(luò)操作經(jīng)常屬于這一類)。因?yàn)檫@個(gè)PR,運(yùn)行時(shí)現(xiàn)在會(huì)利用實(shí)現(xiàn)了IThreadPoolWorkItem的異步狀態(tài)機(jī)避免這種情況下的分配。
除此之外,用在async/await的IThreadPoolWorkItem允許asnyc實(shí)現(xiàn)將任務(wù)以一種像其他代碼那樣更加內(nèi)存友好的行為排進(jìn)線程池隊(duì)列,還進(jìn)行了一些更改,讓線程池獲得關(guān)于狀態(tài)機(jī)裝箱的第一手資料來幫助它優(yōu)化更多案例。benaadams的這個(gè)PR讓線程池把一些UnsafeQueueUserWorkItem(Action<object>, object, bool)調(diào)用在底層換成UnsafeQueueUserWorkItem(IAsyncStateMachineBox, bool),這樣更高層的類庫就可以享受到這樣分配的好處,而不必意識到裝箱機(jī)制。
另一個(gè)異步相關(guān)的領(lǐng)域是Timer類型的有效優(yōu)化。在DNC2.1中,System.Threading.Timers得到了一些一些重要的優(yōu)化,以提高吞吐量和降低競爭時(shí)長,來應(yīng)對一種普遍情況:計(jì)時(shí)器沒有觸發(fā),相反它很快就被新建和銷毀了。雖然這些改動(dòng)在計(jì)時(shí)器實(shí)際觸發(fā)時(shí)起到了一點(diǎn)作用,但是并沒有解決掉主要的消耗和競爭的源頭——持有鎖的時(shí)候進(jìn)行了很多潛在工作(與注冊的計(jì)時(shí)器數(shù)量成比例),DNC3作出了很大的改進(jìn)。這個(gè)PR將注冊計(jì)時(shí)器的內(nèi)部鏈表分成兩部分:一個(gè)鏈表存儲很快就會(huì)觸發(fā)的計(jì)時(shí)器,另一個(gè)存儲一段時(shí)間不觸發(fā)的計(jì)時(shí)器。在大多數(shù)工作負(fù)載下,都會(huì)有很多計(jì)時(shí)器被注冊,大部分在任何給定的時(shí)間點(diǎn)上都會(huì)扔到下一個(gè)桶里,這個(gè)分區(qū)方案則允許了運(yùn)行時(shí)在大多數(shù)時(shí)間只考慮觸發(fā)計(jì)時(shí)器的小桶。這樣做顯著減少了涉及觸發(fā)計(jì)時(shí)器的消耗,也引起了持有鎖帶來競爭的顯著減少。一個(gè)受無數(shù)活動(dòng)計(jì)時(shí)器帶來的問題所困擾的客戶在嘗試過這些改變之后如是評價(jià)道:
“我們昨天看到了產(chǎn)品的變化,結(jié)果是驚人的,減少了99%的鎖競爭,測量到了4-5%的CPU提升,更重要的是我們的服務(wù)可靠性提升了0.15%(很大了)!"這個(gè)解決方案的自身情況在測評中難以看出影響,所以我們做了點(diǎn)別的,測量了一些間接影響的東西而不是測量實(shí)際改變的參數(shù)。這些改動(dòng)并不直接影響創(chuàng)建和銷毀計(jì)時(shí)器的性能;實(shí)際上,它們的設(shè)計(jì)目標(biāo)是避免創(chuàng)建和銷毀(尤其是避免破壞重要的過程)。通過減少觸發(fā)計(jì)時(shí)器的消耗減少持有鎖的時(shí)間,也減少了創(chuàng)建銷毀計(jì)時(shí)器帶來的競爭,所以我們的測試建立了一堆計(jì)時(shí)器,測量它們的觸發(fā)時(shí)間和頻率,然后我們測試了創(chuàng)建和銷毀一堆計(jì)時(shí)器的時(shí)間消耗。
100萬個(gè)計(jì)時(shí)器帶來的消耗與優(yōu)化Timer的優(yōu)化也采用了別的形式。例如,benaadams的這個(gè)PR把不用CancellationToken時(shí),涉及Task.Delay的內(nèi)存分配減少了24字節(jié),這個(gè)PR則減少了創(chuàng)建計(jì)時(shí)的CancellationTokenSource的分配,對吞吐量帶來了不錯(cuò)的影響:
甚至還有更低層次的優(yōu)化已經(jīng)投入生產(chǎn),舉個(gè)蠣子,benaadams的這個(gè)優(yōu)化了Thread.CurrentThread的PR,將有意義的線程存放在ThreadStatic字段中,而不是強(qiáng)制CurrentThread在運(yùn)行時(shí)的native部分生成一個(gè)InternalCall。
還有些別的蠣子,這個(gè)PR“教會(huì)了”運(yùn)行時(shí)要“尊重Docker的 -cpu限制”,這個(gè)PR和另一個(gè)PR優(yōu)化了通過各種同步站點(diǎn)(原文synchronization site)帶來競爭時(shí)的自旋行為,這個(gè)PR則優(yōu)化了SemaphoreSlim,當(dāng)一個(gè)實(shí)例消費(fèi)者把同步Wait和異步WaitAsync混合起來時(shí)。Quogu的這個(gè)PR專門為CancellationTokenSource創(chuàng)建了一個(gè)0延時(shí),以避免Timer相關(guān)的損耗。
集合
把臭腳從線程上拿開,讓我們來看看集合帶來的優(yōu)化。集合在每個(gè)程序上都有普遍的應(yīng)用,因此它們在以前的DNC版本中得到了很多性能上的關(guān)注,即使這樣,仍然有提升的一席之地,下面是DNC3中的一些例子。
ConcurrentDictionary<TKey, TValue>有一個(gè)IsEmpty屬性,標(biāo)記了當(dāng)前狀態(tài)下,字典是不是空的,在以前的版本中,它持有所有字典的鎖來獲取即時(shí)狀態(tài),但實(shí)際上,只有我們認(rèn)為集合可能是空的時(shí)候,鎖才需要被持有;如果集合的任何內(nèi)部桶有任何元素,就不需要有鎖,而且只要找到一個(gè)有元素的散列桶,就不需要查找別的桶。因此,drewnoakes的這個(gè)PR添加了一個(gè)快速流程,首先檢查沒有鎖的散列桶,來優(yōu)化字典非空這種普遍情況(字典是空的帶來的影響很小)。
優(yōu)化了24倍,是dnc3做得太好,還是dnc2做得太差?ConcurrentDictionary并不是唯一做出優(yōu)化的并行集合。這個(gè)PR給了ConcurrentQueue<T>一個(gè)優(yōu)化,這是個(gè)有趣的例子,表明性能優(yōu)化通常是場景之間的權(quán)衡。在DNC2中,我們大改了ConcurrentQueue的實(shí)現(xiàn),顯著提高其吞吐量,也明顯減少了內(nèi)存分配,將ConcurrentQueue換成了一個(gè)循環(huán)數(shù)組鏈表,但是這一改動(dòng)涉及到讓步:因?yàn)閿?shù)組的生產(chǎn)者/消費(fèi)者的天性,如果有任何需要監(jiān)視鏈表分段中的數(shù)據(jù)操作(而不是將其踢出隊(duì)列),被監(jiān)視的分段就會(huì)為任何接下來的排隊(duì)而被“凍結(jié)”……這一舉措是為了避免這樣的事例:一個(gè)線程在枚舉段(segment)內(nèi)的元素,而另一個(gè)線程則在進(jìn)行入隊(duì)和出隊(duì),當(dāng)這個(gè)隊(duì)列有很多段時(shí)對Count的訪問最后被視為觀察對象,但是那就意味著對ConcurrentQueue.Count的簡單訪問將會(huì)渲染隊(duì)列中的所有段以進(jìn)一步排隊(duì),此時(shí)我們認(rèn)為這樣的權(quán)衡可以了,因?yàn)閼?yīng)該沒人會(huì)足夠頻繁的訪問隊(duì)列的計(jì)數(shù),然后我們想錯(cuò)了,幾個(gè)客戶報(bào)告了工作負(fù)載中顯著的遲緩,因?yàn)樗麄冊诿總€(gè)入隊(duì)和出隊(duì)時(shí)都獲取了隊(duì)列計(jì)數(shù)。雖然正確的解決方法是不這樣做,但我們?nèi)韵胄迯?fù)這個(gè)問題。實(shí)際上,這個(gè)修復(fù)相對簡單直觀,這樣我們就可以在性能上同時(shí)得到魚和熊掌,結(jié)果在下面的測試中很明顯了:
ImmutableDictionary<TKey, TValue>也得到了我們的注意。(譯者注:我相信這是FP開發(fā)者最喜歡的東西了)一個(gè)客戶跟我們說他們比較了ImmutableDictionary<TKey, TValue>和Dictionary<TKey, TValue>,發(fā)現(xiàn)前者查找的性能遠(yuǎn)比后者慢,這事其實(shí)在意料之中,因?yàn)檫@兩個(gè)類型用到了大不一樣的數(shù)據(jù)結(jié)構(gòu),ImmutableDictionary的優(yōu)化點(diǎn)在廉價(jià)地創(chuàng)建一個(gè)字典的可變副本,一些操作相對于Dictionary來說太昂貴;一開始的權(quán)衡就說查找會(huì)更慢一些,但是我們還是看了一下ImmutableDictionary查找的性能,然后這個(gè)PR提供了幾個(gè)提升性能的修改,將一個(gè)遞歸調(diào)用變成了非遞歸和可內(nèi)聯(lián),并去掉了一些無謂的結(jié)構(gòu)體包裝。這雖然沒讓ImmutableDictionary和Dictionary的查找功能速度一樣,但是也讓ImmutableDictionary 性能大有提升,尤其是在它只有幾個(gè)元素的時(shí)候。
另一個(gè)在DNC3中看到顯著提升的集合是BitArray。很多操作包括構(gòu)造器,都在這個(gè)PR里優(yōu)化了。
這一集合的核心操作,例如Get和Set在這個(gè)omariom的PR里得到了進(jìn)一步提升,通過流水線化相關(guān)的方法,并使其可以內(nèi)聯(lián)。(譯者注:這個(gè)PR有些雞肋,因?yàn)锽itArray屬于System.Collections命名空間,而這個(gè)命名空間應(yīng)該被拋棄)
另一個(gè)例子是SortedSet<T>。acerbusace的這個(gè)PR更改了GetViewBetween修改整個(gè)集合和子集的計(jì)數(shù)管理方式,得到了漂亮的性能加速。
比較器在DNC3中也有漂亮的性能提升,如這個(gè)PR重寫了運(yùn)行時(shí)中枚舉類型的比較器實(shí)現(xiàn)方式,借用了CoreRT中使用的方法。性能優(yōu)化經(jīng)常是添加代碼;而這是偶然發(fā)生的,優(yōu)化代碼不僅更快,還更簡單更小的情況。
網(wǎng)絡(luò)
從運(yùn)行在System.Net.Sockets和System.Net.Security的kestrel服務(wù)器,到通過HttpClient訪問web服務(wù)的網(wǎng)絡(luò)應(yīng)用,System.Net已經(jīng)成為了許多應(yīng)用的必備之選,在DNC2.1中它收到了很多優(yōu)化嘗試,3版本也是一樣。
讓我們先來看看HttpClient。這個(gè)PR所做的優(yōu)化圍繞緩沖處理方式進(jìn)行,特別是在服務(wù)器提供內(nèi)容長度(ContentLength)時(shí),作為復(fù)制響應(yīng)數(shù)據(jù)的一部分的大緩沖請求場合。在一次快速連接和一個(gè)大的響應(yīng)數(shù)據(jù)體情況下(例子中是10MB),因?yàn)闇p少了系統(tǒng)對傳輸數(shù)據(jù)的調(diào)用,吞吐量有很大的差別。
現(xiàn)在看看SslStream。以前的版本看上去把SslStream上的讀寫優(yōu)化到頭了,但是在DNC3中的這兩個(gè)PR(還有一個(gè)給Unix的)讓連接的初始化更有效率,特別是在分配方面。
在System.Net.Sockets中有個(gè)利用先前說過的IThreadPoolWorkItem的例子。在Windows上的異步操作中,我們用了“重疊I/O”,使用I/O線程池中的線程去執(zhí)行socket操作的延續(xù)操作;Windows將I/O完成包隊(duì)列化,然后I/O池線程開始執(zhí)行,包括調(diào)用延續(xù)。但是在Unix上機(jī)制就大不一樣,沒有“重疊I/O”,相反,System.Net.Sockets中的異步是通過epoll(或者macos上的kqueues)進(jìn)行的,系統(tǒng)的所有socket以一個(gè)epoll文件描述符的形式注冊,有一個(gè)線程監(jiān)視著epoll的變化。不管一個(gè)針對socket的異步操作什么時(shí)候完成,epoll都會(huì)被標(biāo)記,上面阻塞的線程就會(huì)喚醒并執(zhí)行。如果該線程繼續(xù)運(yùn)行socket的延續(xù)動(dòng)作,那么它最終會(huì)無限制的工作下去,阻止其他socket的處理——死鎖。因此與此相反,這個(gè)線程會(huì)把一個(gè)工作者(work item)帶進(jìn)線程池隊(duì)列,然后立刻返回執(zhí)行其他的socket。在DNC3之前,排隊(duì)涉及到分配,因此每個(gè)在unix上異步完成的socket操作都會(huì)有至少一個(gè)分配。在這個(gè)PR中,就再也沒有分配了,因?yàn)橐粋€(gè)實(shí)現(xiàn)了IThreadPoolWorkItem,代表異步操作的緩存對象會(huì)被反復(fù)重用,直接列隊(duì)進(jìn)入線程池。
System.Net的其他領(lǐng)域也從剛才提到的這些工作中受益,例如Dns.GetHostName在它的marshal操作中用的是StringBuilder,但是在這個(gè)PR之后就不再這樣了。
IPAddress.HostToNetworkOrder/NetworkToHostOrder間接從剛才說過的intrinsics推送中受益,在DNC2.1中,BinaryPrimitives.ReverseEndianness作為一個(gè)優(yōu)化過的實(shí)現(xiàn)添加進(jìn)來,IPAddress的方法被重寫成ReverseEndianness的簡單包裝,在DNC3中,這個(gè)PR將ReverseEndianness換成了JIT intrinsic實(shí)現(xiàn),因?yàn)镴IT能夠發(fā)出一個(gè)很有效率的BSWAP指令,使得IPAddress的吞吐量有所提高。
提升了100多倍,夠毀三觀的System.IO(I/O操作)
壓縮和網(wǎng)絡(luò)通信一直是“手拉手”的關(guān)系,因此壓縮操作在DNC3中也優(yōu)化了。最值得注意的是,一個(gè)關(guān)鍵性的依賴被更新了。在Unix上,System.IO.Compression只用了機(jī)器上可用的zlib,而且zlib是幾乎每個(gè)unix發(fā)行版的標(biāo)準(zhǔn)部分。但是在Windows上,zlib幾乎都找不到,因此它被內(nèi)置在win版的DNC里面。現(xiàn)在我們不包著標(biāo)準(zhǔn)的zlib了,DNC帶了一個(gè)Intel的優(yōu)化魔改版(還沒有合并給上游,指標(biāo)準(zhǔn)zlib),在DNC3中,我們同步到zlib-intel的最新版,1.2.11,這個(gè)庫帶來了一些很可觀的性能優(yōu)化,尤其是解壓縮上。
也有利用了DNC上述優(yōu)化的壓縮相關(guān)案例,比如同步方法Stream.CopyTo以前不是虛方法,但是重寫了異步的CopyToAsync方法并針對混合流類型(concrete stream types)優(yōu)化后,CopyTo被設(shè)定為虛方法,來靠重寫得到相似的優(yōu)化。這個(gè)PR在DeflateStream上覆寫了CopyTo,本質(zhì)上減少了和zlib的互操作消耗。
BrotliStream也作出了相應(yīng)的改進(jìn)(在DNC3中也被HttpClient用來自動(dòng)解壓Brotli編碼的內(nèi)容),以前每個(gè)新的BrotliStream都會(huì)分配一個(gè)很大的緩存,但是在這個(gè)PR中,緩沖被池化了,就像在DeflateStream中做的一樣(另外,這個(gè)PR重寫了BrotliStream的ReadByte和WriteByte以避免父類實(shí)現(xiàn)的分配)。
把視線從壓縮上移開,應(yīng)用于多場合下的格式化可比只格式化基元類型更值得介紹,例如TextWriter有很多編寫格式化字串的方法,比如public override void Write(string format, object arg0, arg1),這個(gè)PR針對StreamWriter優(yōu)化了這個(gè)方法,通過提供特定的重寫使其更有效率,減少分配:
再舉個(gè)例子,TomerWeisberg提的這個(gè)PR在BinaryReader包含MemoryStream時(shí),通過將普遍情況特殊處理,來提高BinaryReader的基元類型解析性能。
再來看看MarcoRossignoli提的這個(gè)PR,對StringWriter的Write{Line}{Async}方法添加了覆寫,引入了一個(gè)StringBuilder參數(shù)。StringWriter只是一個(gè)StringBuilder的包裝,而且StringBuilder知道如何把自己和另一個(gè)StringBuilder加起來,因此這些StringWriter的重寫可以直接通過。
System.IO.Pipelines是另一個(gè)在DNC3中受到很多關(guān)注的類庫。Pipelines在DNC2.1中就引入了,作為I/O管線的一部分提供了緩沖管理,被ASP.NET?Core大量應(yīng)用。不少PR用來提高它的性能,例如這玩意將普遍情況特殊處理,默認(rèn)情況下,MemoryPool<byte>.Shared作為默認(rèn)的Pool給一個(gè)Pipe使用。Pipe會(huì)直接訪問底層的ArrayPool<byte>.Shared,繞過Memory<byte>.Shared,移除了一個(gè)間接層,還有MemoryPool<byte>.Rent返回的IMemoryOwner<byte>對象開銷(注意這個(gè)測試,因?yàn)镾ystem.IO.Pipelines是Nuget的一部分,而不是在公共框架中,我添加了一個(gè)配置,指定了每次運(yùn)行中使用哪個(gè)包版本):
benaadams的這個(gè)PR允許Pipe使用UnsafeQueueUserWorkIte裝箱相關(guān)的優(yōu)化,這個(gè)PR則避免了排入不重要的工作(work items),那個(gè)PR修改了以前的默認(rèn)情況來優(yōu)化一般情況下的緩存處理,35216號PR在各種pipe操作中減少了切片操作數(shù)量,benaadams的另一個(gè)PR減少了核心操作的鎖數(shù)量,35509號PR減少參數(shù)驗(yàn)證(減少了判斷分支消耗),33000號PR著眼于減少作為主要交換管線的ReadOnlySequence<byte>相關(guān)的消耗,這個(gè)PR進(jìn)一步優(yōu)化了Pipe上GetSpan和Advance之類的操作,最后把已經(jīng)很低的CPU和內(nèi)存開銷再次削減:
System.Console(控制臺)
常人往往不會(huì)認(rèn)為控制臺也是性能敏感的,但是這個(gè)版本中有兩個(gè)改動(dòng),我覺得有必要講講。
一開始,我們聽說了很多關(guān)于控制臺性能的擔(dān)憂,顯而易見地影響到了用戶的體驗(yàn),特別是交互式控制臺應(yīng)用程序在光標(biāo)上做了很多操作,也涉及到查找光標(biāo)在哪的問題。在Windows上,光標(biāo)的獲取和設(shè)定都是很快的操作,通過kernel32.dll暴露出來的函數(shù)P/Invoke即可,但是在unix上,事情就變得復(fù)雜了,沒有標(biāo)準(zhǔn)的POSIX函數(shù)去獲得/設(shè)定一個(gè)終端的光標(biāo)位置,相反有個(gè)標(biāo)準(zhǔn)的習(xí)慣,通過ANSI轉(zhuǎn)義序列去和終端交互。要設(shè)定光標(biāo)位置,需要寫一些字符來輸出(例如"ESC [ 12 ; 34 H"代表12行, 34列),終端會(huì)識別并作出相應(yīng)舉動(dòng)。獲取光標(biāo)位置更是個(gè)考驗(yàn),一個(gè)應(yīng)用需要輸出一個(gè)請求 (例如“ESC [ 6 n”),終端會(huì)回應(yīng)一個(gè)類似于“ESC [ 12 ; 34 R”,代表在12行和34列的光標(biāo)。這一切都意味著要從輸入讀取和解析,因此在Windows上一個(gè)內(nèi)部調(diào)用的事,unix上我們又得讀寫又得解析,而且要防止用戶臉滾鍵盤使用應(yīng)用時(shí)不會(huì)發(fā)生問題(原句為user sitting at a keyboard),這樣的操作并不廉價(jià),如果只是偶爾地獲取光標(biāo)位置,還不是什么大問題,但是當(dāng)頻繁獲取時(shí),原本為Windows寫的操作很廉價(jià)的代碼,在遷移到其他平臺時(shí)就會(huì)發(fā)生肉眼可見的性能問題,不過好在這個(gè)問題在DNC3中已經(jīng)被tmds的這個(gè)PR定位到了,這一改動(dòng)緩存了當(dāng)前位置,然后基于用戶交互手動(dòng)處理緩存值更新,例如輸入文字或者改變窗口大小。注意一點(diǎn),.NET的測試會(huì)重定向標(biāo)準(zhǔn)輸入和輸出,因此這會(huì)讓Console.CursorLeft/Top立刻就返回0,因此針對這個(gè)測試,我用StopWatch做了個(gè)小的控制臺應(yīng)用,如你所見,版本之間差別很大:
1萬多倍,毀三觀在另個(gè)地方,控制臺在unix和windows上性能都有提升,有趣的是這個(gè)改動(dòng)一開始是因?yàn)楣δ?#xff08;尤其是用在Windows上),但是它對所有的操作系統(tǒng)都有性能提升。在.NET中,我們指定緩沖區(qū)大小大多數(shù)情況下是為了性能,也代表著一種權(quán)衡:緩存越小,內(nèi)存消耗就越小,但是需要更多次的操作,相反緩存越大,內(nèi)存消耗也就越大,操作次數(shù)也就越少。緩存大小很少對功能造成影響,但是在控制臺里就不一樣了。在Windows上,從控制臺讀取用ReadFile或者ReadConsole都行,因?yàn)樗鼈兌际墙邮芤粋€(gè)存儲讀取數(shù)據(jù)的緩存的。Windows上默認(rèn)在你開新一行之前,從控制臺的讀取結(jié)果不會(huì)返回,但是Windows也需要個(gè)地方去存儲輸入的數(shù)據(jù),所以它在提供的緩沖區(qū)這樣做。這樣,Windows不會(huì)讓用戶輸入超過緩沖區(qū)大小的字符——用戶能輸入的行長度被緩沖區(qū)所限。由于歷史原因,.NET使用了256字符的緩存區(qū),但是這個(gè)PR把這個(gè)限制放寬到4096字符,更好地匹配了他人的編程環(huán)境,也允許更合理的行長度。但是提升緩存區(qū)大小的同時(shí),相關(guān)的吞吐量也提高了,尤其是用管道從文件讀取到輸入(from files piped to stdin),例如,以前從stdin讀取8k的輸入數(shù)據(jù),需要調(diào)用ReadFile32次,但是4096的緩存區(qū)域就只需要讀取兩次,帶來的性能影響可以在下面看到(這個(gè)用Benchmark.NET也不好測,所以我又用了個(gè)小控制臺應(yīng)用):
System.Diagnostics.Process
在DNC3中,Process類迎來了很多功能提升,尤其在unix上。但是也有幾個(gè)我要說的性能提升。
這個(gè)PR是另一個(gè)引入新的著重于性能的API,同時(shí)用在DNC里提升核心類庫性能的好例子。它是個(gè)MemoryMarshal的低層API,允許高效地從Span讀取結(jié)構(gòu)體,作為System.Diagnostics.Process交互操作中不可缺少的一部分。我喜歡這個(gè)例子,不僅僅因?yàn)樗鼛砹司薮蟮男阅芴嵘?#xff0c;還因?yàn)樗鼈鬟_(dá)了我一直在傳達(dá)的理念:添加他人可消費(fèi)的API,也用這些API促進(jìn)技術(shù)本身。
另一個(gè)例子更有影響力,joshudson的這個(gè)PR將復(fù)制一個(gè)新進(jìn)程的本機(jī)代碼從使用fork函數(shù)換成了vfork函數(shù),vfork的好處在于避免了將父進(jìn)程的頁表復(fù)制到子進(jìn)程中,假設(shè)子進(jìn)程會(huì)通過幾乎立刻執(zhí)行的exec調(diào)用,來重寫一切。fork做的是copy-on-write(奶牛,寫入時(shí)復(fù)制),但是如果進(jìn)程并行地調(diào)節(jié)很多狀態(tài)(例如帶GC運(yùn)行),這么做代價(jià)就很高,還沒什么必要,為了這個(gè)測試,我在test.c中寫了一個(gè)沒有語句的C程序:
用GCC編譯后的結(jié)果LINQ
以前的版本,為了優(yōu)化LINQ我們累死累活,在DNC3中就少了,因?yàn)楹芏嗤ㄓ梅妒揭呀?jīng)被覆蓋了。但是這一版本還是有很多不錯(cuò)的優(yōu)化。
向System.Linq添加新操作的事情已經(jīng)很稀少了,因?yàn)槿魏稳硕伎梢蕴砑訑U(kuò)展方法,簡單地構(gòu)建和發(fā)布他們認(rèn)為有用的擴(kuò)展方法庫(真的有這么幾個(gè)歷史悠久的庫存在),即使這樣,DNC2仍然添加了一個(gè)TakeLast方法。在DNC3中,romasz的這個(gè)PR更新了TakeLast方法,使之和內(nèi)部的IPartition<T>集成,允許幾個(gè)操作互相配合,有助于優(yōu)化(有些情況下效果很大)這些操作的不同用途。
就在最近,這個(gè)PR優(yōu)化了Enumerable.Range(...).Select(…)的常見模板,關(guān)于Range生成的對象針對性地優(yōu)化了Select,還允許Select操作的流跳過遍歷IEnumerable<T>,直接循環(huán)想要的數(shù)值范圍。
Enumerable.Empty<T>()在這個(gè)PR中也被修改了,來更好地配合DNC的System.Linq中已有的優(yōu)化。雖然不應(yīng)該在Enumerable.Empty<T>()的結(jié)果上顯式調(diào)用額外的LINQ操作,但是IEnumerable<T>的返回值可能是一個(gè)Empty<T>()的結(jié)果也很正常,然后調(diào)用者可能在其上進(jìn)行別的操作,因此這個(gè)優(yōu)化很有必要:
我們也很關(guān)注DNC的程序集大小,特別是因?yàn)樗苡绊慉OT編譯,類似于這個(gè)的PR,在有大量泛型的LINQ里面應(yīng)用了“ThrowHelper",幫助減少生成代碼的體積,對不止它自己還有別的領(lǐng)域的性能提升都有好處。
互操作
互操作是對.NET的用戶或者.NET自己都非常重要的事情之一,因?yàn)楹芏?NET的功能需要和操作系統(tǒng)的功能互操作才能正常運(yùn)行。互操作的性能提升影響了很多組件。
一個(gè)值得注意的類是SafeHandle,另一個(gè)把代碼從本機(jī)代碼遷移到托管代碼獲得性能提升的例子,SafeHandle是管理非托管資源生命周期的推薦方式,不管是Windows上的句柄,還是unix上的文件描述符,它在coreclr和corefx的所有托管庫里的內(nèi)部使用方式也是如此。推薦使用它的一個(gè)原因是它會(huì)使用合適的同步機(jī)制確保這些非托管資源不會(huì)在使用時(shí)就被托管代碼關(guān)閉,那就意味著互操作層需要跟蹤SafeHandle搞出來的每一個(gè)P/Invoke,在P/Invoke之前調(diào)用DangerousAddRef,P/Invoke之后調(diào)用DangerousRelease,還需要DangerousGetHandle來提取實(shí)際的指針值,將其傳遞給本機(jī)函數(shù)。在.NET的以前版本中,這些實(shí)現(xiàn)的核心部分是在運(yùn)行時(shí),意味著托管代碼需要在運(yùn)行時(shí)對每個(gè)這樣的操作都對本機(jī)代碼建立InternalCall。在DNC3的這個(gè)PR中,上述操作都被移動(dòng)到托管代碼,移除了過渡操作帶來的開銷。
marshal的優(yōu)化也有幾個(gè)例子。我在這個(gè)帖子里說過幾種StringBuilder用于marshal和互操作的情況,鄭重聲明,我個(gè)人不喜歡在互操作中用StringBuilder,因?yàn)樗黾恿讼暮蛷?fù)雜度,卻沒有多少增益,因此在這個(gè)PR和那個(gè)PR中,我移除了coreclr和corefx的marshal中幾乎所有的StringBuilder,但是還有很多代碼建立在StringBuilder的基礎(chǔ)上,應(yīng)該盡可能地快。這個(gè)PR避免了很多發(fā)生在StringBuilder的marshal操作時(shí)不必要的工作和分配,帶來如下優(yōu)化:
互操作和marshal的特定用途也被優(yōu)化了。例如,FileSystemWatcher在macos上的互操作以前用的是MarshalAs特性,強(qiáng)迫運(yùn)行時(shí)在每個(gè)OS回調(diào)時(shí)都做額外的marshal操作,包括分配數(shù)組。這個(gè)PR把FileSystemWatcher的互操作換成了一個(gè)更有效率的方案,不包括額外的分配,也不包括marshal命令。或者看看這個(gè)PR,System.Drawing也使用了一個(gè)優(yōu)化過的marshal和互操作方案,固定了一個(gè)托管數(shù)組,直接傳遞到非托管代碼,而不是分配多余的內(nèi)存然后復(fù)制進(jìn)去。
花生醬
在這篇帖子的前半部分,我將那些影響了.NET各個(gè)領(lǐng)域的PR分組介紹,其中一些主流功能得到了顯著改進(jìn)。但是也有一些值得關(guān)注且不限領(lǐng)域的PR。
在.NET中我們把這種東西叫“花生醬”,我們有大量的代碼,對于大多數(shù)應(yīng)用程序來說通常都很好,但是它有很多小的改進(jìn)機(jī)會(huì)。單獨(dú)的那些小改進(jìn)不會(huì)讓事情變得更好,但是它們防止了大規(guī)模代碼下的性能滑坡,這樣的問題我們修復(fù)的越多,總體性能也就越好。這兒移除個(gè)分配,那兒少個(gè)循環(huán),還有些沒用的代碼移除了,這些就是我要說的“花生醬”。
提供給Array.Copy的最低下標(biāo)。調(diào)用Array.Copy(src, dst, length)需要運(yùn)行時(shí)為每個(gè)src和dst調(diào)用GetLowerBound,但是傳遞T[]數(shù)組時(shí),下標(biāo)就是0,我們只需要簡單地傳遞0就可以,這個(gè)PR已經(jīng)做了。
復(fù)制到新數(shù)組更廉價(jià)。在很多地方,一個(gè)List<T>存儲了一些數(shù)據(jù)之后,一個(gè)數(shù)組就會(huì)基于list的長度分配出來,然后list的內(nèi)容就會(huì)用CopyTo復(fù)制到數(shù)組中。這個(gè)PR中benaadams意識到了這么做有多SB,然后把它們換成了List.ToArray。
Nullable<T>.Value?vs?GetValueOrDefault.Nullable<T>?有兩個(gè)成員變量來訪問它的值:?Value?和?GetValueOrDefault.反直覺的是?GetValueOrDefault更廉價(jià)一些:?Value需要檢查實(shí)例是不是一個(gè)值,如果不是就拋異常,GetValueOrDefault只會(huì)返回值,如果沒有就是一個(gè)default。這個(gè)PR修改了幾處可以用GetValueOrDefault代替的調(diào)用。
Array.Empty<T>().?在以前的版本中,很多零長度數(shù)組都被換成Array.Empty<T>,在類庫和通過編譯器修改那些類似于params數(shù)組的東西。DNC3延續(xù)了這個(gè)策略,這個(gè)PR對corefx進(jìn)行了一波清理,將更多的0長度數(shù)組換成了緩存的Array.Empty<T>()。
到處避免小分配。?對于新寫的代碼,我們很關(guān)注消耗,在分配上多長了個(gè)心眼,即使分配再小再稀有,都能簡單地?fù)Q成更廉價(jià)的操作。對于已經(jīng)存在的代碼,影響最大的分配會(huì)出現(xiàn)在關(guān)鍵場景的分析中,這些分配會(huì)被盡可能的壓縮。但是還有很多小分配沒有被我們發(fā)現(xiàn),直到我們因?yàn)槟硞€(gè)原因去回顧和評估這些代碼。在每個(gè)版本中,我們都會(huì)移除很多的小分配,例如下面的這些PR,在DNC3中它們用來減少coreclr和corefx的分配:
In System.Collections:?dotnet/corefx#30528
In System.Data:?dotnet/corefx#30130
In System.Data.SqlClient:?dotnet/corefx#34044,?dotnet/corefx#34047,?dotnet/corefx#34234,dotnet/corefx#34999,?dotnet/corefx#35549,?dotnet/corefx#34048,?dotnet/corefx#34390, and?dotnet/corefx#34393, all from @Wraith2
In System.Diagnostics:?dotnet/coreclr#21752
In System.IO:?dotnet/corefx#30509,?dotnet/corefx#30514,?dotnet/coreclr#21760,?dotnet/corefx#37546
In System.Globalization:?dotnet/coreclr#18546,?dotnet/coreclr#21121
In System.Net:?dotnet/corefx#30521,?dotnet/corefx#30530,?dotnet/corefx#30508,?dotnet/corefx#30529,?dotnet/corefx#34356,?dotnet/corefx#36021
In System.Reflection:?dotnet/coreclr#21770,?dotnet/coreclr#21758
In System.Security:?dotnet/corefx#30512,?dotnet/corefx#29612
In System.Uri:?dotnet/corefx#33641,?dotnet/corefx#36056
In System.Xml:?dotnet/corefx#34196
避免顯式靜態(tài)構(gòu)造器。?任何初始化靜態(tài)字段的類型,最后都會(huì)用靜態(tài)構(gòu)造器去初始化,但是如何初始化也會(huì)影響性能。尤其是當(dāng)開發(fā)者顯式編寫了一個(gè)靜態(tài)構(gòu)造器,而不是作為靜態(tài)字段聲明的一部分去初始化字段時(shí),C#編譯器就不會(huì)將類型標(biāo)記成beforefieldinit,beforefieldinit標(biāo)記過的類型對性能有益,因?yàn)樗屵\(yùn)行時(shí)在執(zhí)行初始化時(shí)更加靈活,繼而允許JIT可以更靈活的優(yōu)化,還有訪問這個(gè)類型的靜態(tài)方法時(shí)是不是該加鎖。benadams提交的這個(gè)PR和那個(gè)PR移除了這樣的靜態(tài)構(gòu)造器,這樣可以跨越大量代碼以較小的成本分層。
使用更便宜且滿足功能的代替品。?字符串和Span的IndexOf返回一個(gè)給定元素的位置,Contains只返回是否包含此元素。后者稍稍有效率點(diǎn),因?yàn)樗恍枰櫾氐拇_切位置。因此,很多調(diào)用使用Contains而不是IndexOf,grant-d的這個(gè)PR和另一個(gè)PR指出了這一點(diǎn)。另一個(gè)例子,SocketsHttpHandler(HttpClient背后默認(rèn)的HttpMessageHandler)在判斷一個(gè)鏈接是否要為下次請求重用時(shí),用的是DateTime.UtcNow,但是Environment.TickCount更加廉價(jià),也足以精確地解決這個(gè)問題,因此這個(gè)PR換上了這個(gè)方法。還有個(gè)例子,這個(gè)PR在許多地方修改了Array.Copy的重載,以避免沒用的GetLowerBound()。
簡化互操作。.NET的平臺互操作機(jī)制很強(qiáng)大詳實(shí),給我們留下了很多把柄來指定如何建立調(diào)用,如何傳輸數(shù)據(jù),等等。但是,這些機(jī)制很多都有額外的消耗,例如需要運(yùn)行時(shí)生成一個(gè)marshal存根來執(zhí)行各種需要的轉(zhuǎn)換。這個(gè)PR和另一個(gè)PR,修改了互操作的參數(shù)以避免這種marshal代碼的分配。
避免不必要的全球化。因?yàn)閹缀?0年前設(shè)計(jì)的System.String API,很容易就意外用到涉及到本地文化的字符串比較。這種比較可能是錯(cuò)的,而且消耗也更大,涉及到更多昂貴的操作系統(tǒng)或本地化類庫調(diào)用。特別是以一個(gè)char為參數(shù)的String.IndexOf方法使用了序數(shù)比較,但是以string作為參數(shù)的String.IndexOf使用本地文化執(zhí)行比較。這個(gè)PR指出了System.Net中一堆的這種情況,在這里幾乎總是使用序數(shù)比較(StringComparison.Ordinal),一般在解析基于文本的協(xié)議時(shí)。
避免使用不必要的ExecutionContext?流。?ExecutionContext是環(huán)境狀態(tài)“流”通過程序和異步調(diào)用的主要工具,特別是AsyncLocal <T>. 為了完成這個(gè)流,生成異步操作的代碼(例如Task.Run,Timer,等等)或者當(dāng)其他操作完成時(shí)創(chuàng)建一個(gè)延續(xù)去運(yùn)行的代碼(例如await),需要“捕獲”當(dāng)前的ExecutionContext,將其掛起,之后當(dāng)執(zhí)行相關(guān)的工作時(shí),使用捕獲的ExecutionContext的Run方法繼續(xù)下去。如果在執(zhí)行的工作實(shí)際上不需要ExecutionContext,我們就可以避開它帶來的小分配。這個(gè)PR,33235號PR,33080號PR就是例子:它們把CancellationToken.Register換成了新的CancellationToken.UnsafeRegister,相對于Register唯一的不同就是不走ExecutionContext了。另一個(gè)例子,這個(gè)PR修改了CancellationTokenSource,當(dāng)它創(chuàng)建Timer的時(shí)候,就不會(huì)再捕獲ExecutionContext了,或者看看這個(gè)PR,確保Task完成后,捕獲的ExecutionContext都立刻被丟棄。
集中化/優(yōu)化位操作。benaadams的這個(gè)PR引入了一個(gè)BitOperations類來集中一堆位操作(旋轉(zhuǎn),前導(dǎo)0計(jì)數(shù),對數(shù)等等),后來這個(gè)類型在grant-d的這些PR(22497號,22584號,22630號)里被增強(qiáng)了,System.Private.Corelib的每個(gè)需要位操作的地方,都可以應(yīng)用這些共享的輔助代碼。這確保了所有這樣的調(diào)用(目前是大約70個(gè))都得到了運(yùn)行時(shí)可以集中的最佳實(shí)現(xiàn),不管是利用當(dāng)前硬件的instruction實(shí)現(xiàn),還是利用軟件的。
垃圾回收
不談垃圾回收,就不配稱作談性能的文章。我們提到的很多優(yōu)化都是減少分配的,一部分是直接減少消耗,但更多的是減少GC的負(fù)擔(dān),縮小它要做的工作。但是提升GC自己,也是個(gè)重要問題,就像以前版本那樣,這一版本我們也對其做了工作。
這個(gè)PR包含了不少性能提高,從鎖的優(yōu)化到更好的免費(fèi)list管理。mjsabby的這個(gè)PR添加了大頁面GC的支持(Linux上的“大頁面”),大型應(yīng)用可以選擇這個(gè)優(yōu)化,來消除轉(zhuǎn)換后備緩存(TLB)帶來的瓶頸,這個(gè)PR進(jìn)一步優(yōu)化了GC用的寫屏障。
很重要的一點(diǎn)是優(yōu)化了有很多處理器的機(jī)器的GC行為,例如這個(gè)PR。我在這里就參考Maoni0的這個(gè)博文了:
blogs.msdn.microsoft.com.
類似地,這一版本投入了很多努力去優(yōu)化容器化環(huán)境下執(zhí)行的GC(尤其是嚴(yán)重約束的環(huán)境),例如這個(gè)PR。Maoni0還能做出比我形容的還好的工作,你可以閱讀她的兩篇博文:running-with-server-gc-in-a-small-container-scenario-part-0?和?running-with-server-gc-in-a-small-container-scenario-part-1-hard-limit-for-the-gc-heap.
JIT(動(dòng)態(tài)編譯)
DNC3中的動(dòng)態(tài)編譯進(jìn)行了很多優(yōu)化。
影響最大的改變之一是分層編譯(這一改動(dòng)分散在很多PR中,但是這個(gè)可以作為例子)。分層編譯是MSIL高質(zhì)量編譯成本機(jī)代碼耗時(shí)間問題的解決方案;分析越多,優(yōu)化就越多,時(shí)間也就越長,但是對于一個(gè)在運(yùn)行時(shí)生成代碼的JIT編譯器來說,這個(gè)時(shí)間就是應(yīng)用啟動(dòng)的直接耗時(shí),你將陷入權(quán)衡:你希望花更長的時(shí)間生成更好的代碼,還是希望更快的生成沒那么好的代碼?分層編譯是完成兩個(gè)目標(biāo)的方案。思路是方法首先快速編譯代碼,沒有多少優(yōu)化,然后隨著方法一次次的執(zhí)行,這些方法會(huì)被重新JIT,這一次會(huì)在代碼質(zhì)量上花更多時(shí)間。
有趣的是,分層編譯不僅僅跟啟動(dòng)時(shí)間有關(guān)系。重編譯有第一次編譯所沒有的優(yōu)化,例如分層編譯可以應(yīng)用于可立即運(yùn)行的(R2R)鏡像,這是DNC共享框架中程序集使用的一種預(yù)編譯形式。這些程序集包括了預(yù)編譯的本機(jī)代碼,但是為了版本彈性能用于本機(jī)代碼生成階段的優(yōu)化是有限的,例如跨模塊內(nèi)聯(lián)在R2R中沒有。所以,R2R代碼有助于快速啟動(dòng),但是經(jīng)常使用的方法會(huì)被分層編譯重新編譯,利用這種優(yōu)化會(huì)限制使用原始的預(yù)編譯代碼。
首先我們可以運(yùn)行接下來的測試:
我們可以再次運(yùn)行它,但是這次,通過設(shè)置COMPlus_TieredCompilation環(huán)境變量為0,分層編譯被禁用了。
有很多配置分層編譯的環(huán)境變量,要查看更多細(xì)節(jié),看這里。
另一個(gè)JIT的酷斃提升在這個(gè)PR中出現(xiàn)。在以前的.NET中,JIT會(huì)把一些static readonly聲明的基元類型字段優(yōu)化成常量,例如一個(gè)static readonly int字段被初始化成42,當(dāng)一些用到了這個(gè)字段的代碼被JIT編譯時(shí),JIT編譯器會(huì)把這個(gè)字段代替成const,并進(jìn)行常量折疊和其他可能進(jìn)行的優(yōu)化。在DNC3中,JIT現(xiàn)在可以應(yīng)用static readonly字段的類型來做更多優(yōu)化,例如一個(gè)static readonly字段以父類聲明,初始化的卻是子類(IList<T> list=new List<T>()),JIT可能會(huì)查找存儲在字段里對象的實(shí)際類型,當(dāng)調(diào)用它的虛方法時(shí),“去虛擬化”調(diào)用,甚至潛在地內(nèi)聯(lián)它。
這很好的說明了去虛擬化做出的改進(jìn),但是還有其他的改動(dòng),例如20447,20292,20640號PR,和benaadams的PR,合在一起促進(jìn)了ArrayPool<T>.Shared之類的API。
另一個(gè)不錯(cuò)的優(yōu)化在局部變量的清0上。甚至在initlocals標(biāo)記還沒有設(shè)定時(shí)(例如這個(gè)PR,為coreclr和corefx的所有程序集都執(zhí)行了清0),JIT仍然需要將局部變量引用計(jì)數(shù)歸0,所以GC就不會(huì)發(fā)現(xiàn)和誤認(rèn)垃圾,這種清0可以大大加快速度,尤其是大量操作Span的方法。這個(gè)PR和另一個(gè)在這件事上做了些不錯(cuò)的工作。
另一個(gè)例子和結(jié)構(gòu)體有關(guān)。隨著越來越多的人認(rèn)識到性能的重要性特別是在分配上之后,值類型的使用也有了很大的提升,經(jīng)常一個(gè)包裝另一個(gè)。例如,等待一個(gè)ValueTask會(huì)導(dǎo)致在其上調(diào)用GetAwaiter,并返回一個(gè)包裝了ValueTask的ValueTaskAwaiter。這個(gè)PR通過移除不重要的復(fù)制來優(yōu)化這一解決方案。
你tm打錯(cuò)了吧,哪有0.0002ns的時(shí)間?Go比起C#的一大優(yōu)勢在于運(yùn)行時(shí)很小——只有2M左右,C#自己的微型運(yùn)行時(shí)項(xiàng)目CoreRT現(xiàn)在還在“試驗(yàn)階段”,也許要在明年出.NET 5之前才會(huì)完善,而且CoreRT本身不支持高級語法(例如動(dòng)態(tài)加載插件),因此縮小CoreCLR也是勢在必行的。
我們可以看下這個(gè)issue:Reduce size of PublishSingleFile binary · Issue #24397 · dotnet/coreclr
同一個(gè)hello world應(yīng)用,用C++構(gòu)建只要800K,用DNC構(gòu)建竟然需要70M,因?yàn)閷⒋罅繘]用的dll也給打包了進(jìn)去——理想情況下,需要的dll應(yīng)該只有1.67MB,這位同志的話語直擊心靈:
I think there should be a native / inbuilt solution that self contained only copies the required dlls. It should work like all the native compilers. Copying the whole framework was acceptable for .NET Core 2 and previous where applications were meant to be server applications. Now that .NET Core 3 also supports end user desktop applications you cannot share a folder with over 250 files or binaries with over 70mb where the actual application code is less than 50 lines of code.“我覺得應(yīng)該有個(gè)native/內(nèi)置的解決方案讓自包含(self-contained)的應(yīng)用只含有需要的dll文件。它應(yīng)該像所有的native編譯器。把整個(gè)框架放進(jìn)去的行為在DNC2里是可以接受的,那個(gè)時(shí)候DNC2應(yīng)用就是服務(wù)器應(yīng)用,但是現(xiàn)在DNC3支持桌面應(yīng)用了,你不能發(fā)布一個(gè)有250個(gè)文件的文件夾或者一個(gè)70M的二進(jìn)制程序,實(shí)際的代碼才50行”
微軟的人稱會(huì)在DNC3 pre6中引入ILLinker(基于mono linker開發(fā)的東西),來分析不必要的dll并進(jìn)行剔除,目前的mono linker可以將包體縮小60%,但還是有些不夠……好在這個(gè)issue被加入CoreCLR 3的里程碑中了,相信今年9月份的時(shí)候會(huì)迎來改善。
下一步會(huì)怎樣?
在我寫下這個(gè)帖子的時(shí)候,我數(shù)了29個(gè)coreclr中與性能相關(guān)卻懸而未決的PR,和corefx中的8個(gè)。其中的一些很可能在DNC3正式版中被合并——我確定還會(huì)有一些現(xiàn)在沒有開放的PR。簡而言之,即使是DNC2和DNC2.1,以及這篇博文提到的DNC3,還有那些提交給ASP.NET Core使之成為這顆行星上最快的web服務(wù)框架的改進(jìn)加在一起,仍然有無數(shù)的讓性能越來越好的機(jī)會(huì),你也可以幫助實(shí)現(xiàn)。希望這個(gè)文章讓你為DNC3的潛力而興奮,我很期盼看到你的PR,來為美好的未來一起努力!
原文地址:https://zhuanlan.zhihu.com/p/66152703
.NET社區(qū)新聞,深度好文,歡迎訪問公眾號文章匯總?http://www.csharpkit.com?
總結(jié)
以上是生活随笔為你收集整理的.NET Core 3中的性能提升(译文)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: SciSharpCube:容器中的Sci
- 下一篇: 在ASP.Net Core 中使用枚举类