首页 > 其他分享 >.NET性能优化-复用StringBuilder

.NET性能优化-复用StringBuilder

时间:2022-11-04 10:55:06浏览次数:69  
标签:StringBuilderCache StringBuilder builder 复用 ObjectPool NET public

在之前的文章中,我们介绍了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
{
    /// <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会更加方便

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

附录

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

标签:StringBuilderCache,StringBuilder,builder,复用,ObjectPool,NET,public
From: https://www.cnblogs.com/InCerry/p/recycle_stringbuilder.html

相关文章

  • NETCORE - AutoMappper 对象映射
    NETCORE-AutoMappper对象映射                        引用:https://www.dongchuanmin.com/net/1398.html ......
  • ASP.NET Core教程-Configuration(配置)- HTTPS
    更新记录转载请注明出处:2022年11月4日发布。2022年11月1日从笔记迁移到博客。强制使用HTTPS在中间件配置中,使用UseHttpsRedirection中间件。app.UseHttpsRedir......
  • 《ASP.NET Core技术内幕与项目实战》精简集-高级组件4.3:请求数据校验
    本节内容,涉及到8.3(P269-P272),以WebApi说明为主。主要NuGet包:内置命名空间:System.ComponentModel.DataAnnotationsFluentValidation.AspNetCore(数据检验框架)  一、请......
  • c# .net 4.0 发布nuget
    一、下载工具1.下载NuGet命令行工具 https://dist.nuget.org/win-x86-commandline/v5.8.0/nuget.exe 将NuGet配置为系统环境变量或者C:\Windows目录下2.下载NuGetPack......
  • 图解教程P1-c#和.net框架-2022年11月3日
    1.net之前VB,C,C++.WIN32.API,MFC微软基础类库,com组件对象模型。2.2002发布.net第一个版本面向对象,多平台,行业标准,安全性3..net框架构成执行环境CLRcommentlan......
  • asp.net mvc 3.0 动态无损图片压缩,及路由定义
    最新更新:1208211.定义路由2.编写控制器3.编写图片压缩方法4.测试运行---------------------------------------------------1.定义路由,一般写在Globals.cs文件中routes.M......
  • C# .Net MVC Razor 视图静态分页导航栏生成器
    这里贴出3个重要组成部分1.分页导航栏主算法代码(包括@Html扩展方法)2.HTML页面调用代码(包括导航栏样式)3.Controller控制器代码4.测试路径:http://www.****.com/EBusines......
  • C# .NET 获取枚举值的自定义属性(特性)信息
    一、引言枚举为我看日常开发的可读性提供的非常好的支持,但是有时我们需要得到枚举值得描述信息或者是注释(备注)信息比如要获得 TestEmun.aaa属性值得备注AAA,比较不方便得......
  • .Net Excel 的帮助方法
    ///<summary>///读取Excel流到DataSet///</summary>///<paramname="stream">Excel流</param>///<paramname="dict">......
  • BZOJ P3732 Network(Kruskal重构树)
    Network题目描述:给\(N\)个点的无向图\(\left(1\leqN\leq15000\right)\),记为:\(1\dotsN\)图中有\(M\)条边\(\left(1\leqM\leq30000\right)\),第\(i\)......