编写高性能的C#代码(三)使用SPAN
原文來自互聯(lián)網(wǎng),由長沙DotNET技術(shù)社區(qū)編譯。如譯文侵犯您的署名權(quán)或版權(quán),請聯(lián)系小編,小編將在24小時內(nèi)刪除。
作者介紹:
史蒂夫·戈登(Steve Gordon)是Microsoft MVP,Pluralsight的作者,布萊頓(英國西南部城市)的高級開發(fā)人員和社區(qū)負責人。
編寫高性能的C#代碼(三)使用SPAN?
這篇文章繼續(xù)了我有關(guān)編寫高性能C#代碼的系列文章[1]。在本文中,我們將通過介紹Span?類型從上兩篇文章繼續(xù),并通過將其轉(zhuǎn)換為基于Span的版本來重構(gòu)一些現(xiàn)有代碼。我們將使用Benchmark.NET比較這些方法并驗證我們的更改是否改進了代碼。
如果您想遵循示例代碼,可以在GitHub上找到[2]。
什么是SPAN??
Span是C#7.2引入的一種新類型,在.NET Core 2.1運行時中受支持?,F(xiàn)有的.NET Standard 1.0運行時都有一個.NET Standard實現(xiàn),但是在.NET Core中,我將重點介紹運行時更改,以支持可能的最佳版本,也稱為“fast span”。
Span提供對內(nèi)存連續(xù)區(qū)域的類型安全訪問。該內(nèi)存可以位于堆,堆棧上,甚至可以由非托管內(nèi)存組成。Span具有相關(guān)的類型ReadOnlySpan?,該類型提供內(nèi)存中數(shù)據(jù)的只讀視圖。ReadOnlySpan可用于查看不可變類型(例如字符串)占用的內(nèi)存。我更喜歡將Span視為進入某些現(xiàn)有內(nèi)存的窗口,而不管其分配在何處。
圖片在上圖中,Span?引用一些已經(jīng)分配的連續(xù)內(nèi)存?,F(xiàn)在,我們在該內(nèi)存上有了一個窗口。
Span?被定義為引用結(jié)構(gòu),這意味著它僅限于僅在堆棧上分配。這減少了一些潛在的用例,例如將其存儲為類中的字段或在異步方法中使用它。這些限制可以通過使用類似的新型Memory來解決,我們將在以后的文章中介紹它。引用結(jié)構(gòu)設計的主要原因是要確保在使用Span時,我們不會引起其他堆分配。這是它支持高性能代碼路徑中如此高度優(yōu)化的用例的原因之一。
我將避免為這篇文章過多地介紹實現(xiàn)細節(jié)(畢竟這是一篇介紹),而將重點放在一個示例中,我們可能在哪里使用它以及它如何影響我們的基準。
如果您想閱讀有關(guān)Span的更多詳細信息,我建議以下鏈接:
?Span?結(jié)構(gòu)[3]?有關(guān)Span的所有信息:探索新的.NET主體[4]?C#7.2:了解Span[5]?Span By Adam Sitnik[6]
加快現(xiàn)有代碼的速度并減少分配
在上一篇文章中,我們對一些代碼進行了基準測試,這些代碼用于從全名字符串中“解析”姓氏。通過Benchmark.NET,我們確定該方法需要125.8 ns的時間運行,并且每次運行分配160個字節(jié)。
在使用基于Span?的方法進行重構(gòu)之前,我希望這是一個公平的競爭,因此我將首先不使用Span?來優(yōu)化代碼。這有望成為一個很好的例子,因為它著重指出,即使不使用Span之類的新功能,也可以通過對正在執(zhí)行的工作進行一些思考來優(yōu)化現(xiàn)有代碼。
當前代碼在任何空格上分割字符串,這將組成一個字符串數(shù)組。如果考慮到這一點,我們將分配一個數(shù)組,在使用名稱“ Steve J Gordon”的情況下,這樣做時將分配三個較小的字符串“ Steve”,“ J”和“ Gordon”。正如我們在基準測試中所看到的那樣,這會導致分配160個字節(jié)。
對于查找姓氏的要求,我們不在乎存儲名稱的所有部分,而只是存儲我們希望是姓氏的最后一部分。請注意,在此示例中,我忽略了多詞姓氏等情況!
讓我們向NameParser添加另一個方法,該方法而不是拆分字符串,而是獲取最后一個空格字符的索引,并使用該方法獲取代表姓氏的子字符串。
public string GetLastNameUsingSubstring(string fullName){var lastSpaceIndex = fullName.LastIndexOf(" ", StringComparison.Ordinal);return lastSpaceIndex == -1? string.Empty: fullName.Substring(lastSpaceIndex + 1);}首先,我們獲取全名字符串中最后一次出現(xiàn)空格的索引。如果它為-1,則找不到任何空格,因此我們將返回一個空字符串作為默認結(jié)果。如果找到索引,則使用Substring方法提取姓氏并將其返回。
我們稍后會將該版本包含在我們的基準測試中。但是,實際上,值得在進行代碼改進的每個迭代時對其進行測試,以驗證您是在改進方面還是使它們變得更糟。
使用SPAN?
讓我們看看這次如何使用Span?重新編寫此代碼。在高性能需求旺盛的場景中,我們既要提高速度又要減少代碼中的內(nèi)存分配。
public ReadOnlySpan<char> GetLastNameWithSpan(ReadOnlySpan<char> fullName){var lastSpaceIndex = fullName.LastIndexOf(' ');return lastSpaceIndex == -1 ? ReadOnlySpan<char>.Empty : fullName.Slice(lastSpaceIndex + 1);}首先要注意的是,方法參數(shù)“ fullName”現(xiàn)在的類型為ReadOnlySpan?。某些類型(例如字符串)可以隱式轉(zhuǎn)換為chars的ReadOnlySpan,因此此方法簽名可以正常工作?,F(xiàn)在,返回類型也是ReadOnlySpan。
首先,以與上面的優(yōu)化代碼非常相似的方式,我們尋找空格字符的最后一個索引。
同樣,如果其值為-1,則我們找不到空格,并且將返回空的ReadOnlySpan?結(jié)果。
如果找到空格字符,我們現(xiàn)在可以使用Span的一種功能,即“切片”(Slice)。
切片是一項非常強大的操作,我們可以將現(xiàn)有的Span和“切片”放到更緊密的窗口中。切片時,我們?yōu)榍衅付ㄆ鹗嘉恢玫乃饕?#xff0c;并為切片指定終止位置的長度。省略長度會導致從起始位置到Span結(jié)束的切片。
切片是一種低成本的操作,因為我們不復制任何內(nèi)容,而只是創(chuàng)建一個新的Span,該Span表示一個進入現(xiàn)有內(nèi)存范圍子集的窗口。
圖片在上圖中,我們可以創(chuàng)建原始Span的Slice來查看其中的5個元素,而無需分配原始內(nèi)存的任何其他副本。
在新的基于Span的代碼中,我們從空格字符后的索引處開始獲取fullName的一部分。由于我們未指定長度,因此此切片將運行到現(xiàn)有Span的末尾。
對Span進行切片后,會在切片的部分上產(chǎn)生一個新的Span,然后將其作為方法的結(jié)果返回。
至此,我們有兩個潛在的改進代碼版本,一個使用Substring,另一個使用Span?。讓我們更新基準并比較結(jié)果。
衡量改進基準
添加兩個新基準后,基準類現(xiàn)在如下所示:
[RankColumn][Orderer(SummaryOrderPolicy.FastestToSlowest)][MemoryDiagnoser]public class NameParserBenchmarks{private const string FullName = "Steve J Gordon";private static readonly NameParser Parser = new NameParser();[Benchmark(Baseline = true)]public void GetLastName(){Parser.GetLastName(FullName);}[Benchmark]public void GetLastNameUsingSubstring(){Parser.GetLastNameUsingSubstring(FullName);}[Benchmark]public void GetLastNameWithSpan(){Parser.GetLastNameWithSpan(FullName);}}我們定義了三個基準,每個基準在NameParser中采用不同的方法。運行基準測試在我的計算機上給出以下結(jié)果…
圖片此列表中的最后一項是我們原始的GetLastName方法。因為我們要求獲得排名結(jié)果,并且此方法運行的最慢,所以它在最后顯示出來。
這次大約花了125ns的時間運行,當然仍然分配了160個字節(jié)。
第二快的是我們嘗試在不使用Span?的情況下改進代碼的情況,該代碼使用Substring。此代碼比原始方法快大約3倍。重要的是,我們現(xiàn)在將分配減少到只有40個字節(jié)。這說明了我們在調(diào)用子字符串時要分配的姓氏字符串。
總的贏家是基于Span?的方法。這比我們的原始代碼快10倍,比基于子字符串的方法快2.8倍。
這里真正重要的是,因為我們要對Span進行切片以查找姓氏的位置,并且還返回Span作為方法的輸出,所以我們永遠不會分配新的字符串。通過已分配的內(nèi)存狀態(tài)(現(xiàn)在為空)可以明顯看出這一點。
圖片對于單個調(diào)用,節(jié)省的160個字節(jié)(或子字符串方法節(jié)省40個字節(jié))并不龐大,但是在特定場景下上,節(jié)省的費用加起來了。
如果此代碼需要在我維護的每天處理約2000萬條消息的數(shù)據(jù)處理服務中運行,那么我們每天將節(jié)省3.2 GB的分配。這些可能是短暫的分配,但是即使如此,它們?nèi)詫е吕厥?。根?jù)估算的Gen 0 / 1k操作數(shù)(譯者注,是指0代回收,每次回收1k字節(jié)),原始代碼每天將觸發(fā)2,000個操作,共506個GC。
這是CPU時間和暫停時間,我們可以通過避免分配任何資源來幫助減少時間。
摘要
在本文中,我們研究了新的Span?類型,并使用它重構(gòu)了一些代碼以實現(xiàn)最佳性能。最初,Span聽起來可能有點復雜,但正如我希望我已經(jīng)展示的那樣,在本示例中使用它非常簡單。
謝謝閱讀!
如果您想了解有關(guān)高性能.NET和C#代碼的更多信息,可以在此處[7]查看我的完整博客文章系列。
References
[1]?有關(guān)編寫高性能C#代碼的系列文章:?https://www.stevejgordon.co.uk/motivations-for-writing-high-performance-csharp-code
[2]?示例代碼,可以在GitHub上找到:?https://github.com/stevejgordon/BenchmarkAndSpanExample
[3]?Span?結(jié)構(gòu):?https://docs.microsoft.com/en-us/dotnet/api/system.span-1?view=netcore-2.2
[4]?有關(guān)Span的所有信息:探索新的.NET主體:?https://msdn.microsoft.com/en-us/magazine/mt814808.aspx
[5]?C#7.2:了解Span:?https://channel9.msdn.com/Events/Connect/2017/T125
[6]?Span By Adam Sitnik:?https://adamsitnik.com/Span/
[7]?在此處:?https://www.stevejgordon.co.uk/writing-high-performance-csharp-and-dotnet-code
總結(jié)
以上是生活随笔為你收集整理的编写高性能的C#代码(三)使用SPAN的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [Abp vNext微服务实践] - 搭
- 下一篇: .NET Core开发实战(第22课:异