.NET性能優(yōu)化ValueStringBuilder拼接字符串使用實例
前言
這一次要和大家分享的一個Tips是在字符串拼接場景使用的,我們經(jīng)常會遇到有很多短小的字符串需要拼接的場景,在這種場景下及其的不推薦使用String.Concat也就是使用+=運算符。
目前來說官方最推薦的方案就是使用StringBuilder來構建這些字符串,那么有什么更快內存占用更低的方式嗎?那就是今天要和大家介紹的ValueStringBuilder。
ValueStringBuilder
ValueStringBuilder不是一個公開的API,但是它被大量用于.NET的基礎類庫中,由于它是值類型的,所以它本身不會在堆上分配,不會有GC的壓力。
微軟提供的ValueStringBuilder有兩種使用方式,一種是自己已經(jīng)有了一塊內存空間可供字符串構建使用。這意味著你可以使用??臻g,也可以使用堆空間甚至非托管堆的空間,這對于GC來說是非常友好的,在高并發(fā)情況下能大大降低GC壓力。
// 構造函數(shù):傳入一個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
另外一種方式是指定一個容量,它會從默認的ArrayPool的char對象池中獲取緩沖空間,因為使用的是對象池,所以對于GC來說也是比較友好的,千萬需要注意,池中的對象一定要記得歸還。
// 傳入預計的容量
public ValueStringBuilder(int initialCapacity)
{
// 從對象池中獲取緩沖區(qū)
_arrayToReturnToPool = ArrayPool<char>.Shared.Rent(initialCapacity);
......
}
那么我們就來比較一下使用+=、StringBuilder和ValueStringBuilder這幾種方式的性能吧。
// 一個簡單的類
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();
}
}
結果如下所示。

從上圖的結果中,我們可以得出如下的結論。
- 使用
StringConcat是最慢的,這種方式是無論如何都不推薦的。 - 使用
StringBuilder要比使用StringConcat快6.5倍,這是推薦的方法。 - 設置了初始容量的
StringBuilder要比直接使用StringBuilder快25%,正如我在你應該為集合類型設置初始大小一樣,設置初始大小絕對是相當推薦的做法。 - 棧上分配的
ValueStringBuilder比StringBuilder要快50%,比設置了初始容量的StringBuilder還快25%,另外它的GC次數(shù)是最低的。 - 堆上分配的
ValueStringBuilder比StringBuilder要快55%,他的GC次數(shù)稍高與棧上分配。
從上面的結論中,我們可以發(fā)現(xiàn)ValueStringBuilder的性能非常好,就算是在棧上分配緩沖區(qū),性能也比StringBuilder快25%。
源碼解析
ValueStringBuilder的源碼不長,我們挑幾個重要的方法給大家分享一下,部分源碼如下。
// 使用 ref struct 該對象只能在棧上分配
public ref struct ValueStringBuilder
{
// 如果從ArrayPool里分配buffer 那么需要存儲一下
// 以便在Dispose時歸還
private char[]? _arrayToReturnToPool;
// 暫存外部傳入的buffer
private Span<char> _chars;
// 當前字符串長度
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可讀可寫
// 所以重復使用ValueStringBuilder只需將Length設置為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)
{
// 添加字符非常高效 直接設置到對應Span位置即可
int pos = _pos;
if ((uint) pos < (uint) _chars.Length)
{
_chars[pos] = c;
_pos = pos + 1;
}
else
{
// 如果buffer空間不足,那么會走
GrowAndAppend(c);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Append(string? s)
{
if (s == null)
{
return;
}
// 追加字符串也是一樣的高效
int pos = _pos;
// 如果字符串長度為1 那么可以直接像追加字符一樣
if (s.Length == 1 && (uint) pos < (uint) _chars .Length)
{
_chars[pos] = s[0];
_pos = pos + 1;
}
else
{
// 如果是多個字符 那么使用較慢的方法
AppendSlow(s);
}
}
private void AppendSlow(string s)
{
// 追加字符串 空間不夠先擴容
// 然后使用Span復制 相當高效
int pos = _pos;
if (pos > _chars.Length - s.Length)
{
Grow(s.Length);
}
s
#if !NETCOREAPP
.AsSpan()
#endif
.CopyTo(_chars.Slice(pos));
_pos += s.Length;
}
// 對于需要格式化的對象特殊處理
[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)
{
// 單個字符擴容在添加
Grow(1);
Append(c);
}
// 擴容方法
[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倍擴容,默認從對象池中獲取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)
{
// 如果原本就是使用的對象池 那么必須歸還
ArrayPool<char>.Shared.Return(toReturn);
}
}
//
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Dispose()
{
char[]? toReturn = _arrayToReturnToPool;
this = default; // 為了安全,在釋放時置空當前對象
if (toReturn != null)
{
// 一定要記得歸還對象池
ArrayPool<char>.Shared.Return(toReturn);
}
}
}
從上面的源碼我們可以總結出ValueStringBuilder的幾個特征:
- 比起
StringBuilder來說,實現(xiàn)方式非常簡單。 - 一切都是為了高性能,比如各種
Span的用法,各種內聯(lián)參數(shù),以及使用對象池等等。 - 內存占用非常低,它本身就是結構體類型,另外它是
ref struct,意味著不會被裝箱,不會在堆上分配。
適用場景
ValueStringBuilder是一種高性能的字符串創(chuàng)建方式,針對于不同的場景,可以有不同的使用方式。
1.非常高頻次的字符串拼接的場景,并且字符串長度較小,此時可以使用棧上分配的ValueStringBuilder。
大家都知道現(xiàn)在ASP.NET Core性能非常好,在其依賴的內部庫UrlBuilder中,就使用棧上分配,因為棧上分配在當前方法結束后內存就會回收,所以不會造成任何GC壓力。

2.非常高頻次的字符串拼接場景,但是字符串長度不可控,此時使用ArrayPool指定容量的ValueStringBuilder。比如在.NET BCL庫中有很多場景使用,比如動態(tài)方法的ToString實現(xiàn)。從池中分配雖然沒有棧上分配那么高效,但是一樣的能降低內存占用和GC壓力。

3. 非常高頻次的字符串拼接場景,但是字符串長度可控,此時可以棧上分配和ArrayPool分配聯(lián)合使用,比如正則表達式解析類中,如果字符串長度較小那么使用??臻g,較大那么使用ArrayPool。

需要注意的場景
1.在async\await中無法使用ValueStringBuilder。原因大家也都知道,因為ValueStringBuilder是ref struct,它只能在棧上分配,async\await會編譯成狀態(tài)機拆分await前后的方法,所以ValueStringBuilder不好在方法內傳遞,不過編譯器也會警告。

2.無法將ValueStringBuilder作為返回值返回,因為在當前棧上分配,方法結束后它會被釋放,返回它將指向未知的地址。這個編譯器也會警告。

3.如果要將ValueStringBuilder傳遞給其它方法,那么必須使用ref傳遞,否則發(fā)生值拷貝會存在多個實例。這個編譯器不會警告,但是你必須非常注意。

4. 如果使用棧上分配,那么Buffer大小控制在5KB內比較穩(wěn)妥,至于為什么需要這樣,后面有機會在講一講。
總結
今天和大家分享了一下高性能幾乎無內存占用的字符串拼接結構體ValueStringBuilder,在大多數(shù)的場景還是推薦大家使用。但是要非常注意上面提到的的幾個場景,如果不符合條件,那么大家還是可以使用高效的StringBuilder來進行字符串拼接。
以上就是.NET性能優(yōu)化ValueStringBuilder拼接字符串使用實例的詳細內容,更多關于.NET ValueStringBuilder拼接字符串的資料請關注腳本之家其它相關文章!
相關文章
.NET6使用ImageSharp實現(xiàn)給圖片添加水印
這篇文章主要為大家詳細介紹了.NET6使用ImageSharp實現(xiàn)給圖片添加水印功能的相關資料,文中的示例代碼講解詳細,感興趣的小伙伴可以了解一下2022-12-12
asp.net Checbox在GridView中的應用實例分析
這篇文章主要介紹了asp.net Checbox在GridView中的應用,結合實例形式分析了GridView中添加與使用Checbox的相關技巧,具有一定參考借鑒價值,需要的朋友可以參考下2016-07-07
ASP.NET MVC 4 中的JSON數(shù)據(jù)交互的方法
本篇文章主要介紹了ASP.NET MVC 4 中的JSON數(shù)據(jù)交互的方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下。2017-04-04
Asp.Net(C#)自動執(zhí)行計劃任務的程序實例分析分享
這篇文章主要介紹了Asp.Net(C#)自動執(zhí)行計劃任務的程序實例分析,有需要的朋友可以參考一下2014-01-01
Entity Framework加載控制Loading Entities
本文詳細講解了Entity Framework加載控制Loading Entities的用法,文中通過示例代碼介紹的非常詳細。對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-03-03
ASP.NET Core應用錯誤處理之StatusCodePagesMiddleware中間件針對響應碼呈現(xiàn)錯誤頁面
這篇文章主要給大家介紹了關于ASP.NET Core應用錯誤處理之StatusCodePagesMiddleware中間件針對響應碼呈現(xiàn)錯誤頁面的相關資料,需要的朋友可以參考下2019-01-01

