在之前的文章中,我们介绍了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

.NET性能优化-复用StringBuilder的更多相关文章

  1. C#中那些[举手之劳]的性能优化

    隔了很久没写东西了,主要是最近比较忙,更主要的是最近比较懒...... 其实这篇很早就想写了 工作和生活中经常可以看到一些程序猿,写代码的时候只关注代码的逻辑性,而不考虑运行效率 其实这对大多数程序猿 ...

  2. Java 性能优化之 String 篇

    原文:http://www.ibm.com/developerworks/cn/java/j-lo-optmizestring/ Java 性能优化之 String 篇 String 方法用于文本分析 ...

  3. Unity性能优化(3)-官方教程Optimizing garbage collection in Unity games翻译

    本文是Unity官方教程,性能优化系列的第三篇<Optimizing garbage collection in Unity games>的翻译. 相关文章: Unity性能优化(1)-官 ...

  4. Android应用性能优化(转)

    人类大脑与眼睛对一个画面的连贯性感知其实是有一个界限的,譬如我们看电影会觉得画面很自然连贯(帧率为24fps),用手机当然也需要感知屏幕操作的连贯性(尤其是动画过度),所以Android索性就把达到这 ...

  5. android 性能优化

    本章介绍android高级开发中,对于性能方面的处理.主要包括电量,视图,内存三个性能方面的知识点. 1.视图性能 (1)Overdraw简介 Overdraw就是过度绘制,是指在一帧的时间内(16. ...

  6. Android性能优化的浅谈

    一.概要: 本文主要以Android的渲染机制.UI优化.多线程的处理.缓存处理.电量优化以及代码规范等几方面来简述Android的性能优化 二.渲染机制的优化: 大多数用户感知到的卡顿等性能问题的最 ...

  7. 性能优化之Java(Android)代码优化

    最新最准确内容建议直接访问原文:性能优化之Java(Android)代码优化 本文为Android性能优化的第三篇——Java(Android)代码优化.主要介绍Java代码中性能优化方式及网络优化, ...

  8. 那些Android中的性能优化

    性能优化是一个大的范畴,如果有人问你在Android中如何做性能优化的,也许都不知道从哪开始说起. 首先要明白的是,为什么我们的App需要优化,最显而易见的时刻:用户say,什么狗屎,刷这么久都没反应 ...

  9. Java程序性能优化Tip

    本博客是阅读<java time and space performance tips>这本小书后整理的读书笔记性质博客,增加了几个测试代码,代码可以在此下载:java时空间性能优化测试代 ...

随机推荐

  1. [开源内卷] .NET 定时任务 -- FreeScheduler 支持 cron、持久化、可变定时设置

    前言 卷了,卷了,卷了,最近太卷...这篇文章写了好几天了,由于同类型文章太多,排期到今天发布.实在不想卷,得罪了!各位定时任务开源大佬们! .NET 定时组件生态实在太强大了,写下此文只希望能供大家 ...

  2. 「JOI 2015 Final」分蛋糕 2

    「JOI 2015 Final」分蛋糕 2 题解 这道题让我想起了新年趣事之红包这道DP题,这道题和那道题推出来之后的做法是一样的. 我们可以定义dp[i][len][1] 表示从第i块逆时针数len ...

  3. java方法---可变参数

    可变参数 在方法的声明中,在指定参数类型后面加一个...(省略号) 一个方法中只能指定一个可变参数,它必须是方法的最后一个参数,任何普通参数必须在它之前声明:

  4. Python入门系列(一)安装环境

    python是什么 python是一门很受欢迎的语言,除了不能生孩子以外,其它都可以做. 它擅长的领域是脚本工具和科学数据这一块,比如大数据,数据分析什么的. python安装 为了演示和验证教程可用 ...

  5. 踩坑之旅:配置 ROS 环境

    以下内容为本人的著作,如需要转载,请声明原文链接微信公众号「englyf」https://www.cnblogs.com/englyf/p/16660252.html 最近在学习机器人相关的导航算法, ...

  6. Springboot shiro JWT集成总结

    SpringBoot Shiro JWT 1.建表 DDL.sql CREATE TABLE `t_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, ` ...

  7. DFS算法-求集合的所有子集

    目录 1. 题目来源 2. 普通方法 1. 思路 2. 代码 3. 运行结果 3. DFS算法 1. 概念 2. 解题思路 3. 代码 4. 运行结果 4. 对比 1. 题目来源 牛客网,集合的所有子 ...

  8. RedHat Linux升级内核

    操作系统:Red Hat 6.4 内核文件:linux-3.10.1.tar.gz  https://www.cnblogs.com/cherish-sweet/p/newyum.html uname ...

  9. PLG SaaS 产品 Figma 商业模式拆解

    9 月 15 日,Figma 的 CEO Dylan Field 发布消息:今天,Figma 宣布接受 Adobe 的收购... Adobe 以约 200 亿美元收购 Figma,这也是 Adobe ...

  10. 《吐血整理》高级系列教程-吃透Fiddler抓包教程(22)-如何使用Fiddler生成Jmeter脚本-下篇

    1.简介 今天这篇文章其实和上一篇差不多也是利用一个fiddler的插件进行Jmeter脚本的导出,开始宏哥想要合在一起写一篇文章,可是结果实践的时候,两个插件还是有区别的,因此为了不绕晕小伙伴或者童 ...