日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 >

如何编写高性能的C#代码(四)字符串的另类骚操作

發布時間:2023/12/4 46 豆豆
生活随笔 收集整理的這篇文章主要介紹了 如何编写高性能的C#代码(四)字符串的另类骚操作 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

原文來自互聯網,由長沙DotNET技術社區編譯。如譯文侵犯您的署名權或版權,請聯系小編,小編將在24小時內刪除。

作者介紹:

史蒂夫·戈登(Steve Gordon)是Microsoft MVP,Pluralsight的作者,布萊頓(英國西南部城市)的高級開發人員和社區負責人。

在本文中,我將繼續有關編寫高性能C#和.NET代碼的系列文章[1]。這次,我將重點介紹String類型–

String.Create

-一種可用的新方法。.NET Core 2.1中首次引入該方法,目前計劃將該方法發布后作為.NET Standard 2.1的一部分包含在內。

STRING.CREATE做什么?

String.Create方法支持有效創建需要在運行時構建或計算的字符串。在我進一步討論之前,讓我們花一點時間來介紹有關字符串的一些事實。

?在.NET中,字符串是一種流行的類型,用于表示文本數據。?字符串是引用類型,它們的數據存儲在托管堆中。?根據設計,字符串是不可變的,這意味著一旦創建,就無法修改其數據。

從高性能的角度來看,這些事實的結合會導致字符串出現問題。從高層次上講,我們編寫高性能代碼的目標通常是減少運行該代碼的執行時間,并刪除內存分配。

由于其不變性,對字符串進行操作通常會導致分配過多。如果要提取字符串的一部分,則會導致創建新字符串以及在舊字符串和新字符串占用的內存之間復制字符串數據。如果我們想將字符串轉換為大寫,這也會導致在堆上分配新的字符串。

如果我們想使用僅在運行時可用的數據以編程方式創建字符串,則也會出現問題。串聯字符串也將導致分配和復制。對于長字符串,尤其是由許多組成部分組成的字符串,此成本可能會顯著增加。

這并不意味著在適當的時候不應該使用字符串,但是在編寫高度優化的代碼時就成為一個問題。

在運行時構造字符串時使用的標準解決方案是使用StringBuilder,該StringBuilder使用附加了字符的內部緩沖區。當您在StringBuilder上調用build方法時,這將導致最終的字符串分配。

當串聯多個元素時,StringBuilder通常比普通串聯更有效(始終使用基準測試來驗證您的方案)。

StringBuilder仍然需要字符的中間緩沖區,因此在那里要分配堆,并在從緩沖區構建字符串時再加上一個副本。

StringBuilder本身是一個類,因此使用其中的分配。

在ASP.NET Core團隊已經在熱路徑上通過池化和共享StringBuilder的實例來解決這個分配成本問題,這是有意義的,例如在中間件等地方。

什么時候使用STRING.CREATE?

在日常開發過程中,不需要String.Create。它有一個特定的目的,即以高度優化的方式從某些現有數據實用地創建字符串,或者可能僅通過算法來創建字符串。

在這種情況下,主要的優化是幫助我們避免不必要的分配和數據復制。我們將在幾分鐘后看一個可行的示例,但在此之前,讓我們考慮一些更通用的用例。

在用于ASP.NET Core的Kestrel Web服務器中,每個請求都會創建唯一的ID。在這種情況下,要求構建一個長度和格式已知的字符串,該字符串將唯一地標識請求。由于此操作每秒可能完成數千次,因此使其性能良好至關重要。String.Create允許在這種情況下有效地構造字符串。

STRING.CREATE如何工作?

String.Creates提供了一個非常短的窗口,允許我們從本質上打破字符串的不變性規則。這聽起來有些嚇人,但還不如我講的那么糟糕。可能發生數據突變的窗口僅在返回對字符串的第一個引用之前。在此簡短窗口之后,將無法修改現有字符串的數據。

在內部,String.Create在堆上分配適當的內存部分,以包含字符串數據的char數組。為此,該方法將字符串所需的長度作為第一個參數。這是一個重要的限制,您必須知道或能夠預先計算出要創建的字符串的確切字符長度。

這是Create方法的簽名:

public static string Create<TState> (int length, TState state, System.Buffers.SpanAction<char,TState> action);

該方法采用第二個參數,這是構造字符串所需的一般狀態。一會兒我們來專門介紹這個狀態。?

最后,create方法接受一個委托,該委托應在分配的堆內存上進行操作以設置最終的字符串數據。在這種情況下,參數是SpanAction,它在System.Buffers中定義。

由于Span?類型不能用作泛型類型參數,因此不能使用標準的Action委托。相反,SpanAction支持采用將用作內部Span?的類型的類型。在這種情況下,我們正在處理字符。

SpanAction委托就是魔力所在。在分配了字符串所需的char []內存之后,然后可以使用我們傳遞的委托來填充該數組中的字符。委托完成后,將返回內部使用該數組的字符串,并已正確設置其值。

讓我們考慮一下不使用此方法即可構建字符串的最低分配方式之一。我們可能會使用臨時char數組作為緩沖區來構建字符串數據,然后將該數組傳遞給字符串的構造函數。這基本上就是StringBuilder為我們所做的。這種方法將導致兩種分配,一種分配給緩沖區,另一種分配給字符串。所涉及的陣列之間也會發生一些內存復制。

可能是這樣的:

using System;namespace StringCreateSample{class Program{private const char spaceSeparator = ' '; // space separator characterstatic void Main(){// Our source data (state) which will be composed into the final string.var context = new ContextData{FirstString = "Hello",SecondString = ".NET",ThirdString = "friends."};var length = context.FirstString.Length + 1 + context.SecondString.Length + 1 + context.ThirdString.Length;var buffer = new char[length]; // allocationvar position = 0;for (var i = 0; i < context.FirstString.Length; i++){buffer[i] = context.FirstString[i];position++;}buffer[position++] = spaceSeparator;for (var i = 0; i < context.SecondString.Length; i++){buffer[position++] = context.SecondString[i];}buffer[position++] = spaceSeparator;for (var i = 0; i < context.ThirdString.Length; i++){buffer[position++] = context.ThirdString[i];}Console.WriteLine(new string(buffer)); // string allocation + copy}}internal struct ContextData{public string FirstString { get; set; }public string SecondString { get; set; }public string ThirdString { get; set; }}}

另一個選擇是使用不安全的代碼,或者在.NET Core 2.1及更高版本中,我們可以使用Span?支持來安全地使用小的堆棧分配緩沖區,而不是堆分配的數組。

只要緩沖區的大小不是太大,這將是一個不錯的選擇,并且我們將僅針對最后一個字符串進行一次堆分配。

但是,將需要一個副本來將數據從堆棧內存中移到字符串堆內存中。這具有很小的執行時間成本。 我們的示例Main方法中為實現此目的所做的更改如下所示:

static void Main(){// Our source data (state) which will be composed into the final string.var context = new ContextData{FirstString = "Hello",SecondString = ".NET",ThirdString = "friends."};var length = context.FirstString.Length + 1 + context.SecondString.Length + 1 + context.ThirdString.Length;// In real-world code we should ensure we don't try to allocate too much on the stack!// Ignoring that risk for this example.Span<char> buffer = stackalloc char[length]; // DOES NOT heap allocatevar position = 0;for (var i = 0; i < context.FirstString.Length; i++){buffer[i] = context.FirstString[i];position++;}buffer[position++] = spaceSeparator;for (var i = 0; i < context.SecondString.Length; i++){buffer[position++] = context.SecondString[i];}buffer[position++] = spaceSeparator;for (var i = 0; i < context.ThirdString.Length; i++){buffer[position++] = context.ThirdString[i];}Console.WriteLine(new string(buffer)); // string allocation + copy from stack memory}

回到String.Create,我們現在可以了解這如何為我們提供最佳性能。通過避免對字符進行預緩沖(即使該字符在堆棧中),這意味著用于構造字符串的邏輯將直接作用于該字符串將引用的存儲器的最終區域。

正確完成后,我們可以以編程方式構建字符串,而無需中間分配,并且具有很高的性能。 在SpanAction中,我們可以通過字符串占用的內存訪問Span?。我們可以通過Span修改該內存,將其切成適當的位置并將字符寫入基礎數組。

傳入的狀態將允許我們使用現有數據來構建字符串。您可能已經在想一個重要的問題。為什么將狀態直接傳遞給Create方法?為什么我們不能僅僅從委托代碼中引用我們需要的數據?

原因是,如果我們捕獲變量,則后一種方法將導致關閉。編譯器將必須生成一個類來處理此問題,這是我們在此處要避免的堆分配。

另外,這里的關閉將防止委托的緩存,這本身就是我們無法承受的性能損失。相反,Create方法接受狀態作為參數,以避免委托形成閉包。

解釋起來有點復雜,但是這里的要點是確保狀態中需要包含為創建字符串而需要訪問的所有對象。

如果要傳遞多個對象,建議的模式是使用ValueTuple[2]。由于這是一個結構,因此它不會分配任何內容,一旦進入委托,您就可以對其進行解構以獲取組成部分。

使用STRING.CREATE的快速示例

在深入研究真實示例之前,讓我們快速看一下如何使用String.Create。

using System;namespace StringCreateSample{class Program{private const char spaceSeparator = ' '; // space separator characterstatic void Main(){// Our source data (state) which will be composed into the final string.var context = new ContextData{FirstString = "Hello",SecondString = ".NET",ThirdString = "friends."};var length = context.FirstString.Length + 1 + context.SecondString.Length + 1 + context.ThirdString.Length;var myString = string.Create(length, context, (chars, state) =>{// NOTE: We don't access the context variable in this delegate since // it would cause a closure and allocation.// Instead we access the state parameter.// will track our position within the string data we are populatingvar position = 0;// copy the first string data to index 0 of the Span<char>state.FirstString.AsSpan().CopyTo(chars);position += state.FirstString.Length; // update the position// add a space in the current position and increement position by 1chars[position++] = spaceSeparator;// copy the second string data to a slice at current positionstate.SecondString.AsSpan().CopyTo(chars.Slice(position)); position += state.SecondString.Length; // update the position// add a space in the current position and increement position by 1chars[position++] = spaceSeparator;// copy the third string data to a slice at current positionstate.ThirdString.AsSpan().CopyTo(chars.Slice(position)); });Console.WriteLine(myString);}}internal struct ContextData{public string FirstString { get; set; }public string SecondString { get; set; }public string ThirdString { get; set; }}}

這段代碼中的注釋逐步說明了正在發生的事情。 從上面我們可以看出一個結論,我們有一個ContextData對象,其中包含三個我們要用來構建最終字符串的字符串。

首先,我們計算最終字符串所需的長度,該長度包括組成部分及其之間的間距。

我們將長度傳遞給string.Create并將上下文作為狀態參數傳遞。

最后,我們定義SpanAction委托的代碼,該代碼切片為基礎Span?,以將組件部分復制到最終字符串中的正確位置。所有這些都是通過為字符串所需的內存分配單個堆來實現的。

如何使用STRING.CREATE –一個真實的例子

現在,讓我們根據我遇到的實際情況看一個可行的示例。請注意,這仍然是演示代碼。它基于我的生產要求,但是我已經對其進行了簡化,以便我們可以專注于特定技術。我有把握地確定它可以進一步優化!

在我的演講“Turbocharged: Writing High-Performance C# and .NET Code”中,我討論了一個服務示例,其中,從AWS SQS隊列中讀取消息后,我需要將消息正文存儲到S3存儲桶中。

將內容存儲到S3中時,我們必須為對象提供唯一的鍵。因此,此服務必須計算在上載對象時傳遞到AWS開發工具包的密鑰。在我們的案例中,這種情況每天發生1800萬次,因此即使是很小的性能提升也會對規模產生重大影響。

密鑰由傳入消息中的八個元素組成。最終鍵中僅允許使用小寫字母,數字和下劃線,并且任何空格都應轉換為下劃線。構造字符串的第一種方法是使用數組固定組成部分,然后將各個片段連接在一起以形成最終字符串。我不會在這篇文章中顯示所有代碼,但是您可以在我的GitHub repo中[3]查看一個示例[4]

第二次迭代使用堆棧分配的字符數組作為緩沖區,以形成字符串的最終數據。通過在該內存上使用Span?,我便能夠將各種元素復制到堆棧分配的緩沖區中。在Span?上調用ToString導致創建了對象鍵的最終字符串。再次,我不會在這里顯示該代碼,因為它很長。如果您想簽出,也可以在我的倉庫中[5]找到。

在最后的迭代中,我利用了String.Create,這意味著我可以避免將內存從堆棧分配的緩沖區復制到字符串的堆內存中。如果您想瀏覽該代碼,也可以在我的GitHub repo中找到[6]

請記住,這些樣本尚未完全優化,其設計目的是演示某些特定技術而非完整的優化。在我的案例中,String.Create在運行的基準測試中僅稍快一些。將來,我將對此進行更深入的探討。這是我比較這兩種方法的基準結果。

圖片

在大多數情況下,String.Create方法的速度要快幾納秒,但是在某些基準測試運行中,它的速度要慢幾納秒。潛在地,我可以對轉換邏輯進行一些進一步的優化,從而可以解決這一問題。從邏輯上講,將數據從堆棧內存復制到字符串堆內存所需的工作較少,應該會更有效率,但是對于您的實際情況而言,它始終值得測試。

為了對此進行研究,我對純String.Create和stackalloc創建進行了一些基準測試。對于較短的字符串,stackalloc似乎只快一點。這是一個基準測試,在此基準下,我使用兩種方法將10個字符的短字符串組合在一起。在這種情況下,每個測試中組合的字符串數中的計數。只有五個項目,根本沒有太多。到組合100個字符串時,使用String.Create帶來的性能提升更加明顯。

圖片

如果您對String.Create的另一個示例用例感興趣,我已經在ASP.NET Core基于代碼的地方確定了String.Create應該改善性能的地方。我提出了一個GitHub問題來[7]證明這一點,并希望參與創建PR以提出最終優化方案。

字符串創建最佳實踐

這篇文章中已經有很多信息可以解釋一個方法。最后,讓我們回顧最重要的幾點。

?String.Create提供了一種高性能,低分配的方法來以編程方式創建字符串。?與所有性能優化一樣,對原始解決方案進行基準測試,并確保所做的更改具有積極作用。?避免閉包,并確保不要在SpanAction委托中捕獲外部變量。?使用ValueTuples可以為狀態傳遞多個對象。

STRING.CREATE的局限性

與您可能熟悉的其他一些創建新字符串的方法相比,使用String.Create涉及更多。我不建議在每個地方都使用此功能,但是在性能較高的應用程序中,它可能會提供一些有價值的收益。

您可能遇到的最大限制是,您必須事先知道(或能夠計算)所需字符串的確切長度。您可能需要訪問所有組成狀態對象的長度,以便計算最終字符串的長度。在某些情況下,構建字符串時有很多條件邏輯,僅知道部件的長度可能還不夠。

摘要

String.Create在高性能方案中很有用。一旦了解了它的運行規則,就可以直接使用它。因此,如果您正在優化應用程序中的熱路徑,那么它是一個值得記住的工具,并且在解析和生成字符串(通常是其主要功能的一部分)的應用程序中可能會獲得重大收益。

謝謝閱讀!如果您想了解有關高性能.NET和C#代碼的更多信息,可以在此處[8]查看我的完整博客文章系列。

References

[1]?有關編寫高性能C#和.NET代碼的系列文章:?https://www.stevejgordon.co.uk/writing-high-performance-csharp-and-dotnet-code
[2]?ValueTuple:?https://blogs.msdn.microsoft.com/mazhou/2017/05/26/c-7-series-part-1-value-tuples/
[3]?在我的GitHub repo中:?https://github.com/stevejgordon/TurbochargedDemos/blob/master/src/1%20-%20ObjectKeyBuilderDemo/S3ObjectKeyGenerator.cs
[4]?一個示例:?https://github.com/stevejgordon/TurbochargedDemos/blob/master/src/1%20-%20ObjectKeyBuilderDemo/S3ObjectKeyGenerator.cs
[5]?可以在我的倉庫中:?https://github.com/stevejgordon/TurbochargedDemos/blob/master/src/1%20-%20ObjectKeyBuilderDemo/S3ObjectKeyGeneratorNew.cs
[6]?GitHub repo中找到:?https://github.com/stevejgordon/TurbochargedDemos/blob/master/src/1%20-%20ObjectKeyBuilderDemo/S3ObjectKeyGeneratorNewV2.cs
[7]?提出了一個GitHub問題:?https://github.com/aspnet/AspNetCore/issues/10290
[8]?在此處:?https://www.stevejgordon.co.uk/writing-high-performance-csharp-and-dotnet-code

總結

以上是生活随笔為你收集整理的如何编写高性能的C#代码(四)字符串的另类骚操作的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。