在之前的文章中,我們介紹了 dotnet 在字符串拼接時可以使用的一些性能優化技巧。比如:
為
StringBuilder
設置 Buffer 初始大小使用
ValueStringBuilder
等等 不過這些都多多少少有一些局限性,比如StringBuilder
還是會存在new StringBuilder()
這樣的對象分配(包括內部的 Buffer)。ValueStringBuilder
無法用于async/await
的上下文等等。都不夠的靈活。
那么有沒有一種方式既能像StringBuilder
那樣用于async/await
的上下文中,又能減少內存分配呢?
其實這可以用到存在很久的一個 Tips,那就是想辦法復用StringBuilder
。目前來說復用StringBuilder
推薦兩種方式:
使用 ObjectPool 來創建
StringBuilder
的對象池如果不想單獨創建一個對象池,那么可以使用
StringBuilderCache
使用 ObjectPool 復用
這種方式估計很多小伙伴都比較熟悉,在.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
的系列文章[1]。
使用 StringBuilderCache
另外一個方案就是在.NET 中存在很久的類,如果大家翻閱過.NET 的一些代碼,在有字符串拼接的場景可以經常見到它的身影。但是它和ValueStringBuilder
一樣不是公開可用的,這個類叫StringBuilderCache
。下方所示就是它的源碼,源碼鏈接點擊這里[2]:
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
參考資料
[1]
系列文章: https://www.cnblogs.com/artech/p/object-pool-01.html
[2]源碼鏈接點擊這里: https://github.com/dotnet/runtime/blob/main/src/libraries/Common/src/System/Text/StringBuilderCache.cs