.NET性能優(yōu)化ValueStringBuilder拼接字符串使用實(shí)例
前言
這一次要和大家分享的一個(gè)Tips是在字符串拼接場(chǎng)景使用的,我們經(jīng)常會(huì)遇到有很多短小的字符串需要拼接的場(chǎng)景,在這種場(chǎng)景下及其的不推薦使用String.Concat
也就是使用+=
運(yùn)算符。
目前來說官方最推薦的方案就是使用StringBuilder
來構(gòu)建這些字符串,那么有什么更快內(nèi)存占用更低的方式嗎?那就是今天要和大家介紹的ValueStringBuilder
。
ValueStringBuilder
ValueStringBuilder
不是一個(gè)公開的API,但是它被大量用于.NET的基礎(chǔ)類庫(kù)中,由于它是值類型的,所以它本身不會(huì)在堆上分配,不會(huì)有GC的壓力。
微軟提供的ValueStringBuilder
有兩種使用方式,一種是自己已經(jīng)有了一塊內(nèi)存空間可供字符串構(gòu)建使用。這意味著你可以使用??臻g,也可以使用堆空間甚至非托管堆的空間,這對(duì)于GC來說是非常友好的,在高并發(fā)情況下能大大降低GC壓力。
// 構(gòu)造函數(shù):傳入一個(gè)Span的Buffer數(shù)組 public ValueStringBuilder(Span<char> initialBuffer); // 使用方式: // ??臻g var vsb = new ValueStringBuilder(stackalloc char[512]); // 普通數(shù)租 var vsb = new ValueStringBuilder(new char[512]); // 使用非托管堆 var length = 512; var ptr = NativeMemory.Alloc((nuint)(512 * Unsafe.SizeOf<char>())); var span = new Span<char>(ptr, length); var vsb = new ValueStringBuilder(span); ..... NativeMemory.Free(ptr); // 非托管堆用完一定要Free
另外一種方式是指定一個(gè)容量,它會(huì)從默認(rèn)的ArrayPool
的char
對(duì)象池中獲取緩沖空間,因?yàn)槭褂玫氖菍?duì)象池,所以對(duì)于GC來說也是比較友好的,千萬需要注意,池中的對(duì)象一定要記得歸還。
// 傳入預(yù)計(jì)的容量 public ValueStringBuilder(int initialCapacity) { // 從對(duì)象池中獲取緩沖區(qū) _arrayToReturnToPool = ArrayPool<char>.Shared.Rent(initialCapacity); ...... }
那么我們就來比較一下使用+=
、StringBuilder
和ValueStringBuilder
這幾種方式的性能吧。
// 一個(gè)簡(jiǎn)單的類 public class SomeClass { public int Value1; public int Value2; public float Value3; public double Value4; public string? Value5; public decimal Value6; public DateTime Value7; public TimeOnly Value8; public DateOnly Value9; public int[]? Value10; } // Benchmark類 [MemoryDiagnoser] [HtmlExporter] [Orderer(SummaryOrderPolicy.FastestToSlowest)] public class StringBuilderBenchmark { private static readonly SomeClass Data; static StringBuilderBenchmark() { var baseTime = DateTime.Now; Data = new SomeClass { Value1 = 100, Value2 = 200, Value3 = 333, Value4 = 400, Value5 = string.Join('-', Enumerable.Range(0, 10000).Select(i => i.ToString())), Value6 = 655, Value7 = baseTime.AddHours(12), Value8 = TimeOnly.MinValue, Value9 = DateOnly.MaxValue, Value10 = Enumerable.Range(0, 5).ToArray() }; } // 使用我們熟悉的StringBuilder [Benchmark(Baseline = true)] public string StringBuilder() { var data = Data; var sb = new StringBuilder(); sb.Append("Value1:"); sb.Append(data.Value1); if (data.Value2 > 10) { sb.Append(" ,Value2:"); sb.Append(data.Value2); } sb.Append(" ,Value3:"); sb.Append(data.Value3); sb.Append(" ,Value4:"); sb.Append(data.Value4); sb.Append(" ,Value5:"); sb.Append(data.Value5); if (data.Value6 > 20) { sb.Append(" ,Value6:"); sb.AppendFormat("{0:F2}", data.Value6); } sb.Append(" ,Value7:"); sb.AppendFormat("{0:yyyy-MM-dd HH:mm:ss}", data.Value7); sb.Append(" ,Value8:"); sb.AppendFormat("{0:HH:mm:ss}", data.Value8); sb.Append(" ,Value9:"); sb.AppendFormat("{0:yyyy-MM-dd}", data.Value9); sb.Append(" ,Value10:"); if (data.Value10 is null or {Length: 0}) return sb.ToString(); for (int i = 0; i < data.Value10.Length; i++) { sb.Append(data.Value10[i]); } return sb.ToString(); } // StringBuilder使用Capacity [Benchmark] public string StringBuilderCapacity() { var data = Data; var sb = new StringBuilder(20480); sb.Append("Value1:"); sb.Append(data.Value1); if (data.Value2 > 10) { sb.Append(" ,Value2:"); sb.Append(data.Value2); } sb.Append(" ,Value3:"); sb.Append(data.Value3); sb.Append(" ,Value4:"); sb.Append(data.Value4); sb.Append(" ,Value5:"); sb.Append(data.Value5); if (data.Value6 > 20) { sb.Append(" ,Value6:"); sb.AppendFormat("{0:F2}", data.Value6); } sb.Append(" ,Value7:"); sb.AppendFormat("{0:yyyy-MM-dd HH:mm:ss}", data.Value7); sb.Append(" ,Value8:"); sb.AppendFormat("{0:HH:mm:ss}", data.Value8); sb.Append(" ,Value9:"); sb.AppendFormat("{0:yyyy-MM-dd}", data.Value9); sb.Append(" ,Value10:"); if (data.Value10 is null or {Length: 0}) return sb.ToString(); for (int i = 0; i < data.Value10.Length; i++) { sb.Append(data.Value10[i]); } return sb.ToString(); } // 直接使用+=拼接字符串 [Benchmark] public string StringConcat() { var str = ""; var data = Data; str += ("Value1:"); str += (data.Value1); if (data.Value2 > 10) { str += " ,Value2:"; str += data.Value2; } str += " ,Value3:"; str += (data.Value3); str += " ,Value4:"; str += (data.Value4); str += " ,Value5:"; str += (data.Value5); if (data.Value6 > 20) { str += " ,Value6:"; str += data.Value6.ToString("F2"); } str += " ,Value7:"; str += data.Value7.ToString("yyyy-MM-dd HH:mm:ss"); str += " ,Value8:"; str += data.Value8.ToString("HH:mm:ss"); str += " ,Value9:"; str += data.Value9.ToString("yyyy-MM-dd"); str += " ,Value10:"; if (data.Value10 is not null && data.Value10.Length > 0) { for (int i = 0; i < data.Value10.Length; i++) { str += (data.Value10[i]); } } return str; } // 使用棧上分配的ValueStringBuilder [Benchmark] public string ValueStringBuilderOnStack() { var data = Data; Span<char> buffer = stackalloc char[20480]; var sb = new ValueStringBuilder(buffer); sb.Append("Value1:"); sb.AppendSpanFormattable(data.Value1); if (data.Value2 > 10) { sb.Append(" ,Value2:"); sb.AppendSpanFormattable(data.Value2); } sb.Append(" ,Value3:"); sb.AppendSpanFormattable(data.Value3); sb.Append(" ,Value4:"); sb.AppendSpanFormattable(data.Value4); sb.Append(" ,Value5:"); sb.Append(data.Value5); if (data.Value6 > 20) { sb.Append(" ,Value6:"); sb.AppendSpanFormattable(data.Value6, "F2"); } sb.Append(" ,Value7:"); sb.AppendSpanFormattable(data.Value7, "yyyy-MM-dd HH:mm:ss"); sb.Append(" ,Value8:"); sb.AppendSpanFormattable(data.Value8, "HH:mm:ss"); sb.Append(" ,Value9:"); sb.AppendSpanFormattable(data.Value9, "yyyy-MM-dd"); sb.Append(" ,Value10:"); if (data.Value10 is not null && data.Value10.Length > 0) { for (int i = 0; i < data.Value10.Length; i++) { sb.AppendSpanFormattable(data.Value10[i]); } } return sb.ToString(); } // 使用ArrayPool 堆上分配的StringBuilder [Benchmark] public string ValueStringBuilderOnHeap() { var data = Data; var sb = new ValueStringBuilder(20480); sb.Append("Value1:"); sb.AppendSpanFormattable(data.Value1); if (data.Value2 > 10) { sb.Append(" ,Value2:"); sb.AppendSpanFormattable(data.Value2); } sb.Append(" ,Value3:"); sb.AppendSpanFormattable(data.Value3); sb.Append(" ,Value4:"); sb.AppendSpanFormattable(data.Value4); sb.Append(" ,Value5:"); sb.Append(data.Value5); if (data.Value6 > 20) { sb.Append(" ,Value6:"); sb.AppendSpanFormattable(data.Value6, "F2"); } sb.Append(" ,Value7:"); sb.AppendSpanFormattable(data.Value7, "yyyy-MM-dd HH:mm:ss"); sb.Append(" ,Value8:"); sb.AppendSpanFormattable(data.Value8, "HH:mm:ss"); sb.Append(" ,Value9:"); sb.AppendSpanFormattable(data.Value9, "yyyy-MM-dd"); sb.Append(" ,Value10:"); if (data.Value10 is not null && data.Value10.Length > 0) { for (int i = 0; i < data.Value10.Length; i++) { sb.AppendSpanFormattable(data.Value10[i]); } } return sb.ToString(); } }
結(jié)果如下所示。
從上圖的結(jié)果中,我們可以得出如下的結(jié)論。
- 使用
StringConcat
是最慢的,這種方式是無論如何都不推薦的。 - 使用
StringBuilder
要比使用StringConcat
快6.5倍,這是推薦的方法。 - 設(shè)置了初始容量的
StringBuilder
要比直接使用StringBuilder
快25%,正如我在你應(yīng)該為集合類型設(shè)置初始大小一樣,設(shè)置初始大小絕對(duì)是相當(dāng)推薦的做法。 - 棧上分配的
ValueStringBuilder
比StringBuilder
要快50%,比設(shè)置了初始容量的StringBuilder
還快25%,另外它的GC次數(shù)是最低的。 - 堆上分配的
ValueStringBuilder
比StringBuilder
要快55%,他的GC次數(shù)稍高與棧上分配。
從上面的結(jié)論中,我們可以發(fā)現(xiàn)ValueStringBuilder
的性能非常好,就算是在棧上分配緩沖區(qū),性能也比StringBuilder
快25%。
源碼解析
ValueStringBuilder
的源碼不長(zhǎng),我們挑幾個(gè)重要的方法給大家分享一下,部分源碼如下。
// 使用 ref struct 該對(duì)象只能在棧上分配 public ref struct ValueStringBuilder { // 如果從ArrayPool里分配buffer 那么需要存儲(chǔ)一下 // 以便在Dispose時(shí)歸還 private char[]? _arrayToReturnToPool; // 暫存外部傳入的buffer private Span<char> _chars; // 當(dāng)前字符串長(zhǎng)度 private int _pos; // 外部傳入buffer public ValueStringBuilder(Span<char> initialBuffer) { // 使用外部傳入的buffer就不使用從pool里面讀取的了 _arrayToReturnToPool = null; _chars = initialBuffer; _pos = 0; } public ValueStringBuilder(int initialCapacity) { // 如果外部傳入了capacity 那么從ArrayPool里面獲取 _arrayToReturnToPool = ArrayPool<char>.Shared.Rent(initialCapacity); _chars = _arrayToReturnToPool; _pos = 0; } // 返回字符串的Length 由于Length可讀可寫 // 所以重復(fù)使用ValueStringBuilder只需將Length設(shè)置為0 public int Length { get => _pos; set { Debug.Assert(value >= 0); Debug.Assert(value <= _chars.Length); _pos = value; } } ...... [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Append(char c) { // 添加字符非常高效 直接設(shè)置到對(duì)應(yīng)Span位置即可 int pos = _pos; if ((uint) pos < (uint) _chars.Length) { _chars[pos] = c; _pos = pos + 1; } else { // 如果buffer空間不足,那么會(huì)走 GrowAndAppend(c); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Append(string? s) { if (s == null) { return; } // 追加字符串也是一樣的高效 int pos = _pos; // 如果字符串長(zhǎng)度為1 那么可以直接像追加字符一樣 if (s.Length == 1 && (uint) pos < (uint) _chars .Length) { _chars[pos] = s[0]; _pos = pos + 1; } else { // 如果是多個(gè)字符 那么使用較慢的方法 AppendSlow(s); } } private void AppendSlow(string s) { // 追加字符串 空間不夠先擴(kuò)容 // 然后使用Span復(fù)制 相當(dāng)高效 int pos = _pos; if (pos > _chars.Length - s.Length) { Grow(s.Length); } s #if !NETCOREAPP .AsSpan() #endif .CopyTo(_chars.Slice(pos)); _pos += s.Length; } // 對(duì)于需要格式化的對(duì)象特殊處理 [MethodImpl(MethodImplOptions.AggressiveInlining)] public void AppendSpanFormattable<T>(T value, string? format = null, IFormatProvider? provider = null) where T : ISpanFormattable { // ISpanFormattable非常高效 if (value.TryFormat(_chars.Slice(_pos), out int charsWritten, format, provider)) { _pos += charsWritten; } else { Append(value.ToString(format, provider)); } } [MethodImpl(MethodImplOptions.NoInlining)] private void GrowAndAppend(char c) { // 單個(gè)字符擴(kuò)容在添加 Grow(1); Append(c); } // 擴(kuò)容方法 [MethodImpl(MethodImplOptions.NoInlining)] private void Grow(int additionalCapacityBeyondPos) { Debug.Assert(additionalCapacityBeyondPos > 0); Debug.Assert(_pos > _chars.Length - additionalCapacityBeyondPos, "Grow called incorrectly, no resize is needed."); // 同樣也是2倍擴(kuò)容,默認(rèn)從對(duì)象池中獲取buffer char[] poolArray = ArrayPool<char>.Shared.Rent((int) Math.Max((uint) (_pos + additionalCapacityBeyondPos), (uint) _chars.Length * 2)); _chars.Slice(0, _pos).CopyTo(poolArray); char[]? toReturn = _arrayToReturnToPool; _chars = _arrayToReturnToPool = poolArray; if (toReturn != null) { // 如果原本就是使用的對(duì)象池 那么必須歸還 ArrayPool<char>.Shared.Return(toReturn); } } // [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Dispose() { char[]? toReturn = _arrayToReturnToPool; this = default; // 為了安全,在釋放時(shí)置空當(dāng)前對(duì)象 if (toReturn != null) { // 一定要記得歸還對(duì)象池 ArrayPool<char>.Shared.Return(toReturn); } } }
從上面的源碼我們可以總結(jié)出ValueStringBuilder
的幾個(gè)特征:
- 比起
StringBuilder
來說,實(shí)現(xiàn)方式非常簡(jiǎn)單。 - 一切都是為了高性能,比如各種
Span
的用法,各種內(nèi)聯(lián)參數(shù),以及使用對(duì)象池等等。 - 內(nèi)存占用非常低,它本身就是結(jié)構(gòu)體類型,另外它是
ref struct
,意味著不會(huì)被裝箱,不會(huì)在堆上分配。
適用場(chǎng)景
ValueStringBuilder
是一種高性能的字符串創(chuàng)建方式,針對(duì)于不同的場(chǎng)景,可以有不同的使用方式。
1.非常高頻次的字符串拼接的場(chǎng)景,并且字符串長(zhǎng)度較小,此時(shí)可以使用棧上分配的ValueStringBuilder
。
大家都知道現(xiàn)在ASP.NET Core性能非常好,在其依賴的內(nèi)部庫(kù)UrlBuilder中,就使用棧上分配,因?yàn)闂I戏峙湓诋?dāng)前方法結(jié)束后內(nèi)存就會(huì)回收,所以不會(huì)造成任何GC壓力。
2.非常高頻次的字符串拼接場(chǎng)景,但是字符串長(zhǎng)度不可控,此時(shí)使用ArrayPool指定容量的ValueStringBuilder
。比如在.NET BCL庫(kù)中有很多場(chǎng)景使用,比如動(dòng)態(tài)方法的ToString實(shí)現(xiàn)。從池中分配雖然沒有棧上分配那么高效,但是一樣的能降低內(nèi)存占用和GC壓力。
3. 非常高頻次的字符串拼接場(chǎng)景,但是字符串長(zhǎng)度可控,此時(shí)可以棧上分配和ArrayPool分配聯(lián)合使用,比如正則表達(dá)式解析類中,如果字符串長(zhǎng)度較小那么使用??臻g,較大那么使用ArrayPool。
需要注意的場(chǎng)景
1.在async\await
中無法使用ValueStringBuilder
。原因大家也都知道,因?yàn)?code>ValueStringBuilder是ref struct
,它只能在棧上分配,async\await
會(huì)編譯成狀態(tài)機(jī)拆分await
前后的方法,所以ValueStringBuilder
不好在方法內(nèi)傳遞,不過編譯器也會(huì)警告。
2.無法將ValueStringBuilder
作為返回值返回,因?yàn)樵诋?dāng)前棧上分配,方法結(jié)束后它會(huì)被釋放,返回它將指向未知的地址。這個(gè)編譯器也會(huì)警告。
3.如果要將ValueStringBuilder
傳遞給其它方法,那么必須使用ref
傳遞,否則發(fā)生值拷貝會(huì)存在多個(gè)實(shí)例。這個(gè)編譯器不會(huì)警告,但是你必須非常注意。
4. 如果使用棧上分配,那么Buffer大小控制在5KB內(nèi)比較穩(wěn)妥,至于為什么需要這樣,后面有機(jī)會(huì)在講一講。
總結(jié)
今天和大家分享了一下高性能幾乎無內(nèi)存占用的字符串拼接結(jié)構(gòu)體ValueStringBuilder
,在大多數(shù)的場(chǎng)景還是推薦大家使用。但是要非常注意上面提到的的幾個(gè)場(chǎng)景,如果不符合條件,那么大家還是可以使用高效的StringBuilder
來進(jìn)行字符串拼接。
以上就是.NET性能優(yōu)化ValueStringBuilder拼接字符串使用實(shí)例的詳細(xì)內(nèi)容,更多關(guān)于.NET ValueStringBuilder拼接字符串的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
.NET6使用ImageSharp實(shí)現(xiàn)給圖片添加水印
這篇文章主要為大家詳細(xì)介紹了.NET6使用ImageSharp實(shí)現(xiàn)給圖片添加水印功能的相關(guān)資料,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一下2022-12-12asp.net Checbox在GridView中的應(yīng)用實(shí)例分析
這篇文章主要介紹了asp.net Checbox在GridView中的應(yīng)用,結(jié)合實(shí)例形式分析了GridView中添加與使用Checbox的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2016-07-07ASP.NET MVC 4 中的JSON數(shù)據(jù)交互的方法
本篇文章主要介紹了ASP.NET MVC 4 中的JSON數(shù)據(jù)交互的方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-04-04Entity Framework加載控制Loading Entities
本文詳細(xì)講解了Entity Framework加載控制Loading Entities的用法,文中通過示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-03-03ASP.NET Core應(yīng)用錯(cuò)誤處理之StatusCodePagesMiddleware中間件針對(duì)響應(yīng)碼呈現(xiàn)錯(cuò)誤頁面
這篇文章主要給大家介紹了關(guān)于ASP.NET Core應(yīng)用錯(cuò)誤處理之StatusCodePagesMiddleware中間件針對(duì)響應(yīng)碼呈現(xiàn)錯(cuò)誤頁面的相關(guān)資料,需要的朋友可以參考下2019-01-01