• .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.ObjectPool
    

    Nuget包中提供了默认的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);
    }
    

    运行结果如下图所示:

    当然,我们在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

    下方所示就是它的源码,源码链接点击这里

    namespace System.Text
    {
        /// 为每个线程提供一个缓存的可复用的StringBuilder的实例
        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;
    
            // 获得一个指定容量的StringBuilder.
            // 如果一个适当大小的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);
            }
    
            /// 如果指定的StringBuilder不是太大,就把它放在缓存中
            public static void Release(StringBuilder sb)
            {
                if (sb.Capacity <= MaxBuilderSize)
                {
                    t_cachedInstance = sb;
                }
            }
    
            /// ToString()的字符串生成器,将其释放到缓存中,并返回生成的字符串。
            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会更加方便

    另外StringBuilderCacheMaxBuilderSizeStringBuilderPoolMaxSize都快可以根据项目类型和使用调整,像我们实际中一般都会调整到256KB甚至更大。

    附录#

    本文源码链接:https://github.com/InCerryGit/RecycleableStringBuilderExample

  • 相关阅读:
    基于uniapp大学生社团活动管理系统python+java+node.js+php微信小程序
    Nature Microbiology|溃疡性结肠炎的罪魁祸首:普通拟杆菌的蛋白酶
    【基于pyAudioKits的Python音频信号处理项目(二)】深度学习语音识别
    微信小程序——标签wxml、样式wxss、js、json
    OpenSSL ca证书命令操作详解
    无胁科技-TVD每日漏洞情报-2022-8-4
    MySQL 开启 binlog 日志
    <img>图片格式类型
    html主页框架,前端首页通用架构,layui主页架构框架,首页框架模板
    Java设计模式之装饰器模式
  • 原文地址:https://www.cnblogs.com/InCerry/p/recycle_stringbuilder.html