.NET性能優化-復用StringBuilder

在之前的文章中,我們介紹了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.ObjectPoolNuget包中提供了默認的StringBuilder池化策略StringBuilderPooledObjectPolicyCreateStringBuilderPool()方法,我們可以直接使用它來創建一個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);}運行結果如下圖所示:
.NET性能優化-復用StringBuilder

文章插圖
當然,我們在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的系列文章 。
使用StringBuilderCache另外一個方案就是在.NET中存在很久的類,如果大家翻閱過.NET的一些代碼,在有字符串拼接的場景可以經常見到它的身影 。但是它和ValueStringBuilder一樣不是公開可用的,這個類叫StringBuilderCache 。
.NET性能優化-復用StringBuilder

文章插圖
下方所示就是它的源碼,源碼鏈接點擊這里:
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;}}}

推薦閱讀