在之前的文章中,我们介绍了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. 国家都给NISP证书的补贴了!关于NISP考试的政策有哪些?

    NISP证书由中国信息安全测评中心依据中编办赋予"信息安全服务和信息安全专业人员的能力评估与资质审核"的职能而推出的证书,是中国信息安全测评中心代表国家实施的信息安全人员能力评定证 ...

  2. 简单创建一个SpringCloud2021.0.3项目(一)

    目录 1. 项目说明 1. 版本 2. 用到组件 3. 功能 2. 新建父模块和注册中心 1. 新建父模块 2. 新建注册中心Eureka 3. 新建配置中心Config 4. 新建两个业务服务 1. ...

  3. 至少要几个砝码,可以称出 1g ~ 40g 重量

    请点赞关注,你的支持对我意义重大. Hi,我是小彭.本文已收录到 GitHub · AndroidFamily 中.这里有 Android 进阶成长知识体系,有志同道合的朋友,关注公众号 [彭旭锐] ...

  4. docker注册中心相关操作

    相关命令详解 (1)push推送 将镜像推送到由其名称或标签指定的仓库中.与pull命令相对. [root@docker ~]# docker push --help Usage: docker pu ...

  5. 【Vue学习笔记】—— vue的基础语法 { }

    学习笔记 作者:oMing vue v-on: 简称 @ <div id='app'> <button v-on:click='Show1'> </button> ...

  6. Stream流式计算

    Stream流式计算 集合/数据库用来进行数据的存储 而计算则交给流 /** * 现有5个用户,用一行代码 ,一分钟按以下条件筛选出指定用户 *1.ID必须是偶数 *2.年龄必须大于22 *3.用户名 ...

  7. 配置 Containerd 在 harbor 私有仓库拉取镜像

    官方文档地址:https://github.com/containerd/cri/blob/master/docs/registry.md 严格来说,这个具体可分为两部分 1.在k8s中使用Conta ...

  8. Kubernetes 上部署应用-- 以Wordpress 为例

    用一个 Wordpress 示例来尽可能将前面的知识点串联起来,我们需要达到的目的是让 Wordpress 应用具有高可用.滚动更新的过程中不能中断服务.数据要持久化不能丢失.当应用负载太高的时候能够 ...

  9. MySQL的EXPLAIN会修改数据测试

    文章转载自:https://www.cnblogs.com/kerrycode/p/14138626.html 在博客"Explain命令可能会修改MySQL数据"了解到MySQL ...

  10. 8. Ceph 基础篇 - 运维常用操作

    文章转载自:https://mp.weixin.qq.com/s?__biz=MzI1MDgwNzQ1MQ==&mid=2247485300&idx=1&sn=aacff9f7 ...