自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

.NET 性能優(yōu)化的技巧

開發(fā) 前端
內(nèi)聯(lián)是將方法體(method body)復(fù)制到調(diào)用站點的技術(shù),這樣我們就可以避免跳轉(zhuǎn)、參數(shù)傳遞和寄存器保存/恢復(fù)等繁瑣過程。

最大化內(nèi)聯(lián)

內(nèi)聯(lián)是將方法體(method body)復(fù)制到調(diào)用站點的技術(shù),這樣我們就可以避免跳轉(zhuǎn)、參數(shù)傳遞和寄存器保存/恢復(fù)等繁瑣過程。除了節(jié)省這些之外,內(nèi)聯(lián)還是實現(xiàn)其他優(yōu)化的必要條件。 不不過Roslyn(C#的編譯器)沒有內(nèi)聯(lián)代碼,它是通過JIT實現(xiàn)的,大多數(shù)優(yōu)化也是如此。

[[274382]]

使用靜態(tài)投擲助手(static throw helper)

最近的變化涉及一個重要的重構(gòu),在序列化基準的調(diào)用持續(xù)時間上增加了大約20ns,從~130ns增加到了~150ns。

罪魁禍首是這個助手方法中添加的throw語句:

  1. public static Writer<TBufferWriter> CreateWriter<TBufferWriter>( 
  2.     this TBufferWriter buffer, 
  3.     SerializerSession session) where TBufferWriter : IBufferWriter<byte> 
  4.     if (session == null) throw new ArgumentNullException(nameof(session)); 
  5.     return new Writer<TBufferWriter>(buffer, session); 

當助手方法中包含throw語句時,JIT不會內(nèi)聯(lián)它。解決這個問題的常見技巧是添加一個靜態(tài)的“throw helper”方法,來完成一些棘手的工作,所以最終結(jié)果如下所示:

  1. public static Writer<TBufferWriter> CreateWriter<TBufferWriter>( 
  2.     this TBufferWriter buffer, 
  3.     SerializerSession session) where TBufferWriter : IBufferWriter<byte> 
  4.     if (session == null) ThrowSessionNull(); 
  5.     return new Writer<TBufferWriter>(buffer, session); 
  6.  
  7.     void ThrowSessionNull() => throw new ArgumentNullException(nameof(session)); 

代碼庫在許多地方使用這個技巧,將throw語句放在一個單獨的方法中可能會有其他好處,例如比如改善常用代碼路徑的位置。

最小化虛擬或接口調(diào)用

虛擬調(diào)用比直接調(diào)用慢,如果你正在編寫一個關(guān)鍵系統(tǒng),那么很可能會在分析器中看到虛擬調(diào)用的過程。首先,虛擬調(diào)用需要間接調(diào)用。

去虛擬化是許多JIT編譯器的一個特性,RyuJIT也不例外。然而,這是一個復(fù)雜的功能,并且RyuJIT目前可以證明(自身)方法可以被虛擬化并因此成為內(nèi)聯(lián)的候選者的情況并不多。以下是利用虛擬化的一些常規(guī)技巧,但我確信還有更多。

1. 默認情況下將類標記為sealed,當一個類/方法被標記為sealed時,RyuJIT可以將其考慮在內(nèi)并且可能能夠內(nèi)聯(lián)一個方法調(diào)用。RyuJIT很可能成為下一代的JIT編譯器。64位計算已是大勢所趨,即使它并不總是比32位更快或更有效率。當前的.NET JIT編譯器就是一個使得64位計算機上有時導(dǎo)致程序速度減慢的的例子。但是,這將會被改變:一個新的,下一代x64的JIT編譯器編譯代碼的速度將加快兩倍,它將改變你對64位.NET代碼的印象。

2. 如果可能,將覆蓋(override)方法標記為sealed。override可以翻譯為覆蓋,從字面就可以知道,它是覆蓋了一個方法并且對其重寫,以求達到不同的作用。對我們來說最熟悉的覆蓋就是對接口方法的實現(xiàn),在接口中一般只是對方法進行了聲明,而我們在實現(xiàn)時,就需要實現(xiàn)接口聲明的所有方法。除了這個典型的用法以外,我們在繼承中也可能會在子類覆蓋父類中的方法。

3. 使用具體類型而不是接口,具體類型為JIT提供了更多信息,因此它更有可能內(nèi)聯(lián)你的調(diào)用。

4. 在同一方法中實例化和使用非sealed對象(而不是使用'create'方法),當類型明確已知時,比如構(gòu)造之后,RyuJIT可以對非sealed方法調(diào)用進行虛擬化。

5. 對多態(tài)類型使用泛型類型約束,以便可以使用具體類型對它們進行專門處理,并且可以對接口調(diào)用進行非虛擬化。在Hagar中,我們的核心編寫器類型定義如下:

  1. public ref struct Writer<TBufferWriter> where TBufferWriter : IBufferWriter<byte> 
  2.     private TBufferWriter output
  3.     // --- etc --- 

所有對CIL中Roslyn發(fā)出的輸出方法的調(diào)用之前都會有一條約束指令,該指令告訴JIT,該調(diào)用可以對TBufferWriter上定義的精確方法進行調(diào)用,而不是進行虛擬/接口調(diào)用。這有助于去虛擬化。結(jié)果,所有對在輸出上定義的方法的調(diào)用都被成功地去虛擬化。下面是由JIT團隊的Andy Ayers 編寫的CoreCLR線程,它詳細描述了當前和未來的去虛擬化工作。

減少分配

.NET的垃圾收集器是一項很偉大的項目, 垃圾收集器是 允許對一些無鎖數(shù)據(jù)結(jié)構(gòu)進行算法優(yōu)化,并且還可以刪除整個類的錯誤并減輕開發(fā)人員的認知負擔。總之,垃圾收集是一種非常成功的內(nèi)存管理技術(shù)。

.NET使用bump分配器,其中每個線程通過找到各自的指針來從每個線程上下文中分配對象。因此,當在同一線程上分配和使用短期分配時,可以更好的實現(xiàn)局部緩存(cache locality)機制。

有關(guān).NET 垃圾收集器的更多信息,請點此了解。

對象池(Object Pool) 或緩沖池(Buffer Pool)

Hagar本身并不管理緩沖區(qū),而是將責任轉(zhuǎn)移給用戶。這聽起來可能很麻煩,但實際上并不麻煩,因為它與System.IO.Pipelines兼容。因此,我們可以利用默認管道通過System.Buffers.ArrayPool 提供的高性能緩沖池。

一般來說,重復(fù)使用緩沖區(qū)可以減輕垃圾收集器的壓力。

避免裝箱

盡可能不要通過將值類型轉(zhuǎn)換為引用類型來裝箱值類型。這是常見的建議,但在API設(shè)計中需要考慮一些因素。在Hagar中,可以接受值類型的接口和方法定義是通用的,以便它們可以專門用于精確類型并避免裝箱/拆箱成本。結(jié)果,沒有熱路徑拳擊。在某些情況下仍然存在拳擊,例如異常方法的字符串格式??梢酝ㄟ^對參數(shù)的顯式. tostring()調(diào)用來刪除那些特定的裝箱分配。

減少關(guān)閉分配

只分配閉包一次,并存儲結(jié)果,就可供多次重復(fù)使用。例如,通常將委托傳遞給ConcurrentDictionary. getoradd。與其將委托編寫為內(nèi)聯(lián)lambda,還不如將define定義為類中的私有字段。下面是來自Hagar中可選ISerializable支持包的一個例子:

  1. private readonly Func<Type, Action<object, SerializationInfo, StreamingContext>> createConstructorDelegate; 
  2.  
  3. public ObjectSerializer(SerializationConstructorFactory constructorFactory) 
  4.     // Other parameters/statements omitted. 
  5.     this.createConstructorDelegate = constructorFactory.GetSerializationConstructorDelegate; 
  6.  
  7. // Later, on a hot code path: 
  8. var constructor = this.constructors.GetOrAdd(info.ObjectType, this.createConstructorDelegate); 

盡量減少復(fù)制

.NET Core 2.0和2.1以及最近的C#版本,在刪除數(shù)據(jù)復(fù)制過程的方面取得了相當大的進步。最值得注意的是Span,但在參數(shù)修飾符和只讀結(jié)構(gòu)中也值得一提。Span 是ref 結(jié)構(gòu)堆棧,而不是托管堆上分配。

使用Span來避免數(shù)組分配并避免數(shù)據(jù)復(fù)制

一個Span表示任意內(nèi)存的相鄰區(qū)域, 一個Span實例通常用來保存數(shù)組的元素或數(shù)組的一部分。

對于.NET Core來說,Span對于性能優(yōu)化非常重要,它們使用優(yōu)化的表示來減小它們的大小,這需要添加對內(nèi)部指針的垃圾收集器的支持。內(nèi)部指針是指向數(shù)組范圍內(nèi)的托管引用,而不是只能指向第一個元素,因此需要一個包含數(shù)組偏移量的附加字段。有關(guān)Span的更多信息,請點此參考。

Hagar廣泛使用Span,因為它允許我們創(chuàng)建可用于較大緩沖區(qū)的分段試圖。

通過ref傳遞結(jié)構(gòu)以最小化堆棧上的副本

Hagar使用兩個主要結(jié)構(gòu),Reader 和Writer。這些結(jié)構(gòu)包含幾個字段,幾乎每次調(diào)用都會傳遞給序列化或反序列化調(diào)用路徑。

在沒有干預(yù)的情況下,使用這些結(jié)構(gòu)進行的每個方法調(diào)用都會帶來很大的影響,因為每個調(diào)用都需要將整個結(jié)構(gòu)復(fù)制到堆棧中。

我們可以通過將這些結(jié)構(gòu)作為ref參數(shù)傳遞來避免副本的產(chǎn)生,另外,C#還支持使用ref this作為擴展方法的目標,這非常方便。據(jù)我所知,沒有辦法確保特定的結(jié)構(gòu)類型總是由ref傳遞,如果你不小心在調(diào)用的參數(shù)列表中省略了ref,這可能會導(dǎo)致運行錯誤。

避免保護性拷貝(defensive copy)

Roslyn有時需要做一些工作來保證一些語言不變量,當結(jié)構(gòu)存儲在只讀字段中時,編譯器將插入一些指令,以避免復(fù)制該字段,然后再將其包含到任何能保證不會對其進行修改的操作中。通常,這意味著調(diào)用在結(jié)構(gòu)類型本身上定義的方法,因為將結(jié)構(gòu)作為參數(shù)傳遞給在另一類型上定義的方法已經(jīng)需要將結(jié)構(gòu)復(fù)制到堆棧上(除非通過ref或in傳遞)。

如果將 7.2 添加到csproj文件中,則可以將結(jié)構(gòu)定義為只讀結(jié)構(gòu)(這是c# 7.2的語言特性),則可以避免保護性拷貝。

有時,如果你無法將其定義為只讀結(jié)構(gòu),則最好在其他不可變結(jié)構(gòu)字段上省略readonly修飾符。

以Jon Skeet的NodaTime庫為例,在這個示例中,Jon使大多數(shù)結(jié)構(gòu)變?yōu)橹蛔x,因此能夠?qū)eadonly修飾符添加到包含這些結(jié)構(gòu)的字段中,而不會對性能產(chǎn)生負面影響。

減少分支和分支錯誤預(yù)測

現(xiàn)代cpu依賴于長pipeline的指令,這些指令通過并發(fā)性進行處理。這涉及到CPU分析指令,以確定哪些指令不依賴于前面的指令,還涉及猜測將采用哪些條件跳轉(zhuǎn)語句。為此,CPU使用一個名為分支預(yù)測器(branch predictor)的組件,該組件負責猜測將采用哪個分支。它通常通過讀取和寫入表中的條目來實現(xiàn)這一點,并根據(jù)上次執(zhí)行條件跳轉(zhuǎn)時發(fā)生的情況修改其預(yù)測。

當預(yù)測正確時,就會加快進程,否則就需要把預(yù)測分支的指令排空,重新獲取正確分支的指令進入pipeline繼續(xù)執(zhí)行。

所以加快進程的最好辦法就是減少分支和分支錯誤預(yù)測,首先嘗試最小化分支數(shù)量,如果無法消除分支,請盡量減少錯誤預(yù)測率,這可能涉及使用排序數(shù)據(jù)或重構(gòu)代碼,可以用查找的辦法來代替分支預(yù)測。

其他雜項提示

1. 避免使用LINQ,LINQ在應(yīng)用程序代碼方面很出色,但在庫/框架代碼中很少被用于路徑。LINQ很難對JIT進行優(yōu)化(IEnumerable..),而且傾向于多多分配。

2. 使用具體類型而不是接口或抽象類型,也許最常見的是,如果你在List 上進行迭代,最好不要先將該列表強制轉(zhuǎn)換為IEnumerable (例如,通過使用LINQ或?qū)⑵渥鳛镮Enumerable 參數(shù)傳遞給方法)。這樣做的原因是使用foreach枚舉列表使用非分配List .Enumerator結(jié)構(gòu),但是當它轉(zhuǎn)換為IEnumerable 時,該結(jié)構(gòu)必須被裝箱到IEnumerator for foreach。

3. 反射在庫代碼中特別有用,緩存反射結(jié)果,考慮使用IL或Roslyn為訪問器生成委托,或者更好的方法是使用現(xiàn)有的庫,如Microsoft.Extensions.ObjectMethodExecutor.Sources,Microsoft.Extensions.PropertyHelper.Sources或FastMember。

特定于庫的優(yōu)化

優(yōu)化生成的代碼

Hagar使用Roslyn為要序列化的POCO生成C#代碼,這個C#代碼在編譯時包含在你的項目中。我們可以對生成的代碼執(zhí)行一些優(yōu)化,以加快速度。

通過跳過對已知類型的編解碼器查找來避免虛擬調(diào)用

當復(fù)雜對象包含眾所周知的字段(如int,Guid,string)時,代碼生成器將直接插入對這些類型的手動編碼編解碼器的調(diào)用,而不是調(diào)用CodecProvider來檢索該類型的IFieldCodec 實例。這允許JIT內(nèi)聯(lián)那些調(diào)用,并避免了虛擬/接口間接。

在運行時專門化泛型類型

與上面類似,代碼生成器可以生成在運行時使用專門化的代碼。

預(yù)先計算常數(shù)值以消除某些分支

在序列化期間,每個字段都帶有一個標頭,通常是一個字節(jié)。它會告訴解串器哪個字段是編碼的。此字段標題包含3條信息:字段的規(guī)格(固定寬度、長度前綴、標記分隔、引用等),字段的模式類型(預(yù)期、眾所周知、以前定義的、編碼)用于多態(tài),并將最后3位專用于編碼字段id(如果它小于7)。在許多情況下,可以確切地知道在編譯時這個標頭字節(jié)是什么。如果字段具有值類型,那么我們就知道運行時類型永遠不能與字段類型不同,并且始終知道字段id。

因此,我們通??梢员4嬗嬎銟祟^值所需的所有工作,并可以直接將其作為常量嵌入到代碼中。這樣可以節(jié)省分支并且通常會消除大量的中間語言代碼。

選擇適當?shù)臄?shù)據(jù)結(jié)構(gòu)

通過切換到結(jié)構(gòu)數(shù)組,很大程度上消除了索引和維護集合的成本,并且參考跟蹤不再出現(xiàn)在基準測試中。這有一個缺點,對于大型對象圖,這種新方法可能較慢。

選擇合適的算法

Hagar花費大量時間對可變長度整數(shù)進行編碼/解碼,這種方法被稱為varints,varints是用一個或多個字節(jié)序列化整形的一種方法,以減小有效載荷的大小。許多二進制序列化器使用這種技術(shù),包括協(xié)議緩沖區(qū)。甚至.NET的BinaryWriter也使用這種編碼。下面是參考資料的一小段:

  1. protected void Write7BitEncodedInt(int value) { 
  2.     // Write out an int 7 bits at a time.  The high bit of the byte, 
  3.     // when on, tells reader to continue reading more bytes. 
  4.     uint v = (uint) value;   // support negative numbers 
  5.     while (v >= 0x80) { 
  6.         Write((byte) (v | 0x80)); 
  7.         v >>= 7; 
  8.     } 
  9.     Write((byte)v); 

我想指出ZigZag編碼對于包含負值的有符號整數(shù)可能更有效,而不是強制轉(zhuǎn)換為uint。

這些序列化器中的變量使用稱為Little Endian Base-128或LEB128的算法,該算法每字節(jié)編碼多達7位。它使用每個字節(jié)的最高有效位來指示是否跟隨另一個字節(jié)(1 =是,0 =否)。這是一種簡單的格式,但可能不是最快的。不過PrefixVarint更快,使用PrefixVarint,所有來自LEB128的1都是在有效載荷的開頭一次性寫入的。這可能讓我們使用硬件內(nèi)在函數(shù)來提高這種編碼和解碼的速度。通過將大小信息往前移,我們也可以從有效載荷中一次讀取更多字節(jié),從而減少內(nèi)部壓力并提高性能。

 

責任編輯:武曉燕 來源: 嘶吼
相關(guān)推薦

2018-02-23 13:55:16

ASP.NET性能優(yōu)化技巧

2009-06-16 16:39:49

Hibernate性能

2011-07-11 15:26:49

性能優(yōu)化算法

2013-06-08 14:19:05

性能優(yōu)化KVM

2009-12-09 17:33:22

PHP性能優(yōu)化

2012-07-23 10:22:15

Python性能優(yōu)化優(yōu)化技巧

2019-02-25 07:07:38

技巧React 優(yōu)化

2009-11-27 13:24:20

PHP代碼性能優(yōu)化

2010-07-26 16:35:34

Perl性能

2022-10-09 13:36:44

接口性能優(yōu)化

2024-01-22 13:16:00

接口性能優(yōu)化本地緩存

2011-06-14 14:17:23

性能優(yōu)化系統(tǒng)層次

2011-06-14 11:14:10

性能優(yōu)化代碼

2024-10-09 23:32:50

2011-06-14 14:32:46

性能優(yōu)化

2009-04-16 16:57:58

DotNetNuke優(yōu)化網(wǎng)站開發(fā)

2024-09-09 05:30:00

數(shù)據(jù)庫Spring

2011-06-14 13:48:07

性能優(yōu)化工具

2019-03-21 14:18:38

iOS開發(fā)優(yōu)化原因

2020-03-25 08:00:32

Kubernetes節(jié)點工作
點贊
收藏

51CTO技術(shù)棧公眾號