在之前的文章中,我們介紹了dotnet在字串拼接時可以使用的一些效能優化技巧。比如:
StringBuilder
設定Buffer初始大小ValueStringBuilder
等等StringBuilder
還是會存在new StringBuilder()
這樣的物件分配(包括內部的Buffer)。ValueStringBuilder
無法用於async/await
的上下文等等。都不夠的靈活。那麼有沒有一種方式既能像StringBuilder
那樣用於async/await
的上下文中,又能減少記憶體分配呢?
其實這可以用到存在很久的一個Tips,那就是想辦法複用StringBuilder
。目前來說複用StringBuilder
推薦兩種方式:
StringBuilder
的物件池StringBuilderCache
這種方式估計很多小夥伴都比較熟悉,在.NET Core的時代,微軟提供了非常方便的物件池類ObjectPool
,因為它是一個泛型類,可以對任何型別進行池化。使用方式也非常的簡單,只需要在引入如下nuget包:
dotnet add package Microsoft.Extensions.ObjectPool
Nuget包中提供了預設的StringBuilder
池化策略StringBuilderPooledObjectPolicy
和CreateStringBuilderPool()
方法,我們可以直接使用它來建立一個ObjectPool:
var provider = new DefaultObjectPoolProvider();
// 設定池中StringBuilder初始容量為256
// 最大容量為8192,如果超過8192則不返回池中,讓GC回收
var pool = provider.CreateStringBuilderPool(256, 8192);
var builder = pool.Get();
try
{
for (int i = 0; i < 100; i++)
{
builder.Append(i);
}
builder.ToString().Dump();
}
finally
{
// 將builder歸還到池中
pool.Return(builder);
}
執行結果如下圖所示:
當然,我們在ASP.NET Core等環境中可以結合微軟的依賴注入框架使用它,為你的專案新增如下NuGet包:
dotnet add package Microsoft.Extensions.DependencyInjection
然後就可以寫下面這樣的程式碼,從容器中獲取ObjectPoolProvider
達到同樣的效果:
var objectPool = new ServiceCollection()
.AddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>()
.BuildServiceProvider()
.GetRequiredService<ObjectPoolProvider>()
.CreateStringBuilderPool(256, 8192);
var builder = objectPool.Get();
try
{
for (int i = 0; i < 100; i++)
{
builder.Append(i);
}
builder.ToString().Dump();
}
finally
{
objectPool.Return(builder);
}
更加詳細的內容可以閱讀蔣老師關於ObjectPool
的系列文章。
另外一個方案就是在.NET中存在很久的類,如果大家翻閱過.NET的一些程式碼,在有字串拼接的場景可以經常見到它的身影。但是它和ValueStringBuilder
一樣不是公開可用的,這個類叫StringBuilderCache
。
下方所示就是它的原始碼,原始碼連結點選這裡:
namespace System.Text
{
/// <summary>為每個執行緒提供一個快取的可複用的StringBuilder的範例</summary>
internal static class StringBuilderCache
{
// 這個值360是在與效能專家的討論中選擇的,是在每個執行緒使用盡可能少的記憶體和仍然覆蓋VS設計者啟動路徑上的大部分短暫的StringBuilder建立之間的折衷。
internal const int MaxBuilderSize = 360;
private const int DefaultCapacity = 16; // == StringBuilder.DefaultCapacity
[ThreadStatic]
private static StringBuilder? t_cachedInstance;
// <summary>獲得一個指定容量的StringBuilder.</summary>。
// <remarks>如果一個適當大小的StringBuilder被快取了,它將被返回並清空快取。
public static StringBuilder Acquire(int capacity = DefaultCapacity)
{
if (capacity <= MaxBuilderSize)
{
StringBuilder? sb = t_cachedInstance;
if (sb != null)
{
// 當請求的大小大於當前容量時,
// 通過獲取一個新的StringBuilder來避免Stringbuilder塊的碎片化
if (capacity <= sb.Capacity)
{
t_cachedInstance = null;
sb.Clear();
return sb;
}
}
}
return new StringBuilder(capacity);
}
/// <summary>如果指定的StringBuilder不是太大,就把它放在快取中</summary>
public static void Release(StringBuilder sb)
{
if (sb.Capacity <= MaxBuilderSize)
{
t_cachedInstance = sb;
}
}
/// <summary>ToString()的字串生成器,將其釋放到快取中,並返回生成的字串。</summary>
public static string GetStringAndRelease(StringBuilder sb)
{
string result = sb.ToString();
Release(sb);
return result;
}
}
}
這裡我們又複習了ThreadStatic
特性,用於儲存執行緒唯一的物件。大家看到這個設計就知道,它是存在於每個執行緒的StringBuilder
快取,意味著只要是一個執行緒中需要使用的程式碼都可以複用它,不過它的是複用小於360個字元StringBuilder
,這個能滿足絕大多數場景的使用,當然大家也可以根據自己專案實際情況,調整它的大小。
要使用的話,很簡單,我們只需要把這個類拷貝出來,變成一個公共的類,然後使用相同的測試程式碼即可。
按照慣例,跑個分看看,這裡模擬的是小字串拼接場景:
using System.Text;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Order;
using BenchmarkDotNet.Running;
using Microsoft.Extensions.ObjectPool;
BenchmarkRunner.Run<Bench>();
[MemoryDiagnoser]
[HtmlExporter]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
public class Bench
{
private readonly int[] _arr = Enumerable.Range(0,50).ToArray();
[Benchmark(Baseline = true)]
public string UseStringBuilder()
{
return RunBench(new StringBuilder(16));
}
[Benchmark]
public string UseStringBuilderCache()
{
var builder = StringBuilderCache.Acquire(16);
try
{
return RunBench(builder);
}
finally
{
StringBuilderCache.Release(builder);
}
}
private readonly ObjectPool<StringBuilder> _pool = new DefaultObjectPoolProvider().CreateStringBuilderPool(16, 256);
[Benchmark]
public string UseStringBuilderPool()
{
var builder = _pool.Get();
try
{
return RunBench(builder);
}
finally
{
_pool.Return(builder);
}
}
public string RunBench(StringBuilder buider)
{
for (int i = 0; i < _arr.Length; i++)
{
buider.Append(i);
}
return buider.ToString();
}
}
結果如下所示,和我們想象中的差不多。
根據實際的高效能程式設計來說:
async/await
最佳是使用ValueStringBuilder
,前面文章也說明了這一點StringBuilder
,不要每次都new()
建立它StringBuilderPool
這個池化類StringBuilderCache
會更加方便另外StringBuilderCache
的MaxBuilderSize
和StringBuilderPool
的MaxSize
都快可以根據專案型別和使用調整,像我們實際中一般都會調整到256KB甚至更大。
本文原始碼連結:https://github.com/InCerryGit/RecycleableStringBuilderExample