.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;}}}

推荐阅读