日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程语言 > C# >内容正文

C#

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

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

原文來自互聯(lián)網(wǎng),由長沙DotNET技術(shù)社區(qū)編譯。如譯文侵犯您的署名權(quán)或版權(quán),請聯(lián)系小編,小編將在24小時內(nèi)刪除。

作者介紹:

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

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

String.Create

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

STRING.CREATE做什么?

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

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

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

由于其不變性,對字符串進行操作通常會導(dǎo)致分配過多。如果要提取字符串的一部分,則會導(dǎo)致創(chuàng)建新字符串以及在舊字符串和新字符串占用的內(nèi)存之間復(fù)制字符串?dāng)?shù)據(jù)。如果我們想將字符串轉(zhuǎn)換為大寫,這也會導(dǎo)致在堆上分配新的字符串。

如果我們想使用僅在運行時可用的數(shù)據(jù)以編程方式創(chuàng)建字符串,則也會出現(xiàn)問題。串聯(lián)字符串也將導(dǎo)致分配和復(fù)制。對于長字符串,尤其是由許多組成部分組成的字符串,此成本可能會顯著增加。

這并不意味著在適當(dāng)?shù)臅r候不應(yīng)該使用字符串,但是在編寫高度優(yōu)化的代碼時就成為一個問題。

在運行時構(gòu)造字符串時使用的標準解決方案是使用StringBuilder,該StringBuilder使用附加了字符的內(nèi)部緩沖區(qū)。當(dāng)您在StringBuilder上調(diào)用build方法時,這將導(dǎo)致最終的字符串分配。

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

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

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

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

什么時候使用STRING.CREATE?

在日常開發(fā)過程中,不需要String.Create。它有一個特定的目的,即以高度優(yōu)化的方式從某些現(xiàn)有數(shù)據(jù)實用地創(chuàng)建字符串,或者可能僅通過算法來創(chuàng)建字符串。

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

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

STRING.CREATE如何工作?

String.Creates提供了一個非常短的窗口,允許我們從本質(zhì)上打破字符串的不變性規(guī)則。這聽起來有些嚇人,但還不如我講的那么糟糕??赡馨l(fā)生數(shù)據(jù)突變的窗口僅在返回對字符串的第一個引用之前。在此簡短窗口之后,將無法修改現(xiàn)有字符串的數(shù)據(jù)。

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

這是Create方法的簽名:

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

該方法采用第二個參數(shù),這是構(gòu)造字符串所需的一般狀態(tài)。一會兒我們來專門介紹這個狀態(tài)。?

最后,create方法接受一個委托,該委托應(yīng)在分配的堆內(nèi)存上進行操作以設(shè)置最終的字符串?dāng)?shù)據(jù)。在這種情況下,參數(shù)是SpanAction,它在System.Buffers中定義。

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

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

讓我們考慮一下不使用此方法即可構(gòu)建字符串的最低分配方式之一。我們可能會使用臨時char數(shù)組作為緩沖區(qū)來構(gòu)建字符串?dāng)?shù)據(jù),然后將該數(shù)組傳遞給字符串的構(gòu)造函數(shù)。這基本上就是StringBuilder為我們所做的。這種方法將導(dǎo)致兩種分配,一種分配給緩沖區(qū),另一種分配給字符串。所涉及的陣列之間也會發(fā)生一些內(nèi)存復(fù)制。

可能是這樣的:

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?支持來安全地使用小的堆棧分配緩沖區(qū),而不是堆分配的數(shù)組。

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

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

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,我們現(xiàn)在可以了解這如何為我們提供最佳性能。通過避免對字符進行預(yù)緩沖(即使該字符在堆棧中),這意味著用于構(gòu)造字符串的邏輯將直接作用于該字符串將引用的存儲器的最終區(qū)域。

正確完成后,我們可以以編程方式構(gòu)建字符串,而無需中間分配,并且具有很高的性能。 在SpanAction中,我們可以通過字符串占用的內(nèi)存訪問Span?。我們可以通過Span修改該內(nèi)存,將其切成適當(dāng)?shù)奈恢貌⒆址麑懭牖A(chǔ)數(shù)組。

傳入的狀態(tài)將允許我們使用現(xiàn)有數(shù)據(jù)來構(gòu)建字符串。您可能已經(jīng)在想一個重要的問題。為什么將狀態(tài)直接傳遞給Create方法?為什么我們不能僅僅從委托代碼中引用我們需要的數(shù)據(jù)?

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

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

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

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

使用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; }}}

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

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

我們將長度傳遞給string.Create并將上下文作為狀態(tài)參數(shù)傳遞。

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

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

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

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

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

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

第二次迭代使用堆棧分配的字符數(shù)組作為緩沖區(qū),以形成字符串的最終數(shù)據(jù)。通過在該內(nèi)存上使用Span?,我便能夠?qū)⒏鞣N元素復(fù)制到堆棧分配的緩沖區(qū)中。在Span?上調(diào)用ToString導(dǎo)致創(chuàng)建了對象鍵的最終字符串。再次,我不會在這里顯示該代碼,因為它很長。如果您想簽出,也可以在我的倉庫中[5]找到。

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

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

圖片

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

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

圖片

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

字符串創(chuàng)建最佳實踐

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

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

STRING.CREATE的局限性

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

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

摘要

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

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

References

[1]?有關(guān)編寫高性能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

總結(jié)

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

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。