.NET性能优化-使用ValueStringBuilder拼接字符串
前言
这一次要和大家分享的一个Tips是在字符串拼接场景使用的,我们经常会遇到有很多短小的字符串需要拼接的场景,在这种场景下及其的不推荐使用String.Concat也就是使用+=运算符。
目前来说官方最推荐的方案就是使用StringBuilder来构建这些字符串,那么有什么更快内存占用更低的方式吗?那就是今天要和大家介绍的ValueStringBuilder。
ValueStringBuilder
ValueStringBuilder不是一个公开的API,但是它被大量用于.NET的基础类库中,由于它是值类型的,所以它本身不会在堆上分配,不会有GC的压力。
微软提供的ValueStringBuilder有两种使用方式,一种是自己已经有了一块内存空间可供字符串构建使用。这意味着你可以使用栈空间,也可以使用堆空间甚至非托管堆的空间,这对于GC来说是非常友好的,在高并发情况下能大大降低GC压力。
// 构造函数:传入一个Span的Buffer数组
public ValueStringBuilder(Span<char> initialBuffer);
// 使用方式:
// 栈空间
var vsb = new ValueStringBuilder(stackalloc char[512]);
// 普通数租
var vsb = new ValueStringBuilder(new char[512]);
// 使用非托管堆
var length = 512;
var ptr = NativeMemory.Alloc((nuint)(512 * Unsafe.SizeOf<char>()));
var span = new Span<char>(ptr, length);
var vsb = new ValueStringBuilder(span);
.....
NativeMemory.Free(ptr); // 非托管堆用完一定要Free
另外一种方式是指定一个容量,它会从默认的ArrayPool的char对象池中获取缓冲空间,因为使用的是对象池,所以对于GC来说也是比较友好的,千万需要注意,池中的对象一定要记得归还。
// 传入预计的容量
public ValueStringBuilder(int initialCapacity)
{
// 从对象池中获取缓冲区
_arrayToReturnToPool = ArrayPool<char>.Shared.Rent(initialCapacity);
......
}
那么我们就来比较一下使用+=、StringBuilder和ValueStringBuilder这几种方式的性能吧。
// 一个简单的类
public class SomeClass
{
public int Value1; public int Value2; public float Value3;
public double Value4; public string? Value5; public decimal Value6;
public DateTime Value7; public TimeOnly Value8; public DateOnly Value9;
public int[]? Value10;
}
// Benchmark类
[MemoryDiagnoser]
[HtmlExporter]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
public class StringBuilderBenchmark
{
private static readonly SomeClass Data;
static StringBuilderBenchmark()
{
var baseTime = DateTime.Now;
Data = new SomeClass
{
Value1 = 100, Value2 = 200, Value3 = 333,
Value4 = 400, Value5 = string.Join('-', Enumerable.Range(0, 10000).Select(i => i.ToString())),
Value6 = 655, Value7 = baseTime.AddHours(12),
Value8 = TimeOnly.MinValue, Value9 = DateOnly.MaxValue,
Value10 = Enumerable.Range(0, 5).ToArray()
};
}
// 使用我们熟悉的StringBuilder
[Benchmark(Baseline = true)]
public string StringBuilder()
{
var data = Data;
var sb = new StringBuilder();
sb.Append("Value1:"); sb.Append(data.Value1);
if (data.Value2 > 10)
{
sb.Append(" ,Value2:"); sb.Append(data.Value2);
}
sb.Append(" ,Value3:"); sb.Append(data.Value3);
sb.Append(" ,Value4:"); sb.Append(data.Value4);
sb.Append(" ,Value5:"); sb.Append(data.Value5);
if (data.Value6 > 20)
{
sb.Append(" ,Value6:"); sb.AppendFormat("{0:F2}", data.Value6);
}
sb.Append(" ,Value7:"); sb.AppendFormat("{0:yyyy-MM-dd HH:mm:ss}", data.Value7);
sb.Append(" ,Value8:"); sb.AppendFormat("{0:HH:mm:ss}", data.Value8);
sb.Append(" ,Value9:"); sb.AppendFormat("{0:yyyy-MM-dd}", data.Value9);
sb.Append(" ,Value10:");
if (data.Value10 is null or {Length: 0}) return sb.ToString();
for (int i = 0; i < data.Value10.Length; i++)
{
sb.Append(data.Value10[i]);
}
return sb.ToString();
}
// StringBuilder使用Capacity
[Benchmark]
public string StringBuilderCapacity()
{
var data = Data;
var sb = new StringBuilder(20480);
sb.Append("Value1:"); sb.Append(data.Value1);
if (data.Value2 > 10)
{
sb.Append(" ,Value2:"); sb.Append(data.Value2);
}
sb.Append(" ,Value3:"); sb.Append(data.Value3);
sb.Append(" ,Value4:"); sb.Append(data.Value4);
sb.Append(" ,Value5:"); sb.Append(data.Value5);
if (data.Value6 > 20)
{
sb.Append(" ,Value6:"); sb.AppendFormat("{0:F2}", data.Value6);
}
sb.Append(" ,Value7:"); sb.AppendFormat("{0:yyyy-MM-dd HH:mm:ss}", data.Value7);
sb.Append(" ,Value8:"); sb.AppendFormat("{0:HH:mm:ss}", data.Value8);
sb.Append(" ,Value9:"); sb.AppendFormat("{0:yyyy-MM-dd}", data.Value9);
sb.Append(" ,Value10:");
if (data.Value10 is null or {Length: 0}) return sb.ToString();
for (int i = 0; i < data.Value10.Length; i++)
{
sb.Append(data.Value10[i]);
}
return sb.ToString();
}
// 直接使用+=拼接字符串
[Benchmark]
public string StringConcat()
{
var str = "";
var data = Data;
str += ("Value1:"); str += (data.Value1);
if (data.Value2 > 10)
{
str += " ,Value2:"; str += data.Value2;
}
str += " ,Value3:"; str += (data.Value3);
str += " ,Value4:"; str += (data.Value4);
str += " ,Value5:"; str += (data.Value5);
if (data.Value6 > 20)
{
str += " ,Value6:"; str += data.Value6.ToString("F2");
}
str += " ,Value7:"; str += data.Value7.ToString("yyyy-MM-dd HH:mm:ss");
str += " ,Value8:"; str += data.Value8.ToString("HH:mm:ss");
str += " ,Value9:"; str += data.Value9.ToString("yyyy-MM-dd");
str += " ,Value10:";
if (data.Value10 is not null && data.Value10.Length > 0)
{
for (int i = 0; i < data.Value10.Length; i++)
{
str += (data.Value10[i]);
}
}
return str;
}
// 使用栈上分配的ValueStringBuilder
[Benchmark]
public string ValueStringBuilderOnStack()
{
var data = Data;
Span<char> buffer = stackalloc char[20480];
var sb = new ValueStringBuilder(buffer);
sb.Append("Value1:"); sb.AppendSpanFormattable(data.Value1);
if (data.Value2 > 10)
{
sb.Append(" ,Value2:"); sb.AppendSpanFormattable(data.Value2);
}
sb.Append(" ,Value3:"); sb.AppendSpanFormattable(data.Value3);
sb.Append(" ,Value4:"); sb.AppendSpanFormattable(data.Value4);
sb.Append(" ,Value5:"); sb.Append(data.Value5);
if (data.Value6 > 20)
{
sb.Append(" ,Value6:"); sb.AppendSpanFormattable(data.Value6, "F2");
}
sb.Append(" ,Value7:"); sb.AppendSpanFormattable(data.Value7, "yyyy-MM-dd HH:mm:ss");
sb.Append(" ,Value8:"); sb.AppendSpanFormattable(data.Value8, "HH:mm:ss");
sb.Append(" ,Value9:"); sb.AppendSpanFormattable(data.Value9, "yyyy-MM-dd");
sb.Append(" ,Value10:");
if (data.Value10 is not null && data.Value10.Length > 0)
{
for (int i = 0; i < data.Value10.Length; i++)
{
sb.AppendSpanFormattable(data.Value10[i]);
}
}
return sb.ToString();
}
// 使用ArrayPool 堆上分配的StringBuilder
[Benchmark]
public string ValueStringBuilderOnHeap()
{
var data = Data;
var sb = new ValueStringBuilder(20480);
sb.Append("Value1:"); sb.AppendSpanFormattable(data.Value1);
if (data.Value2 > 10)
{
sb.Append(" ,Value2:"); sb.AppendSpanFormattable(data.Value2);
}
sb.Append(" ,Value3:"); sb.AppendSpanFormattable(data.Value3);
sb.Append(" ,Value4:"); sb.AppendSpanFormattable(data.Value4);
sb.Append(" ,Value5:"); sb.Append(data.Value5);
if (data.Value6 > 20)
{
sb.Append(" ,Value6:"); sb.AppendSpanFormattable(data.Value6, "F2");
}
sb.Append(" ,Value7:"); sb.AppendSpanFormattable(data.Value7, "yyyy-MM-dd HH:mm:ss");
sb.Append(" ,Value8:"); sb.AppendSpanFormattable(data.Value8, "HH:mm:ss");
sb.Append(" ,Value9:"); sb.AppendSpanFormattable(data.Value9, "yyyy-MM-dd");
sb.Append(" ,Value10:");
if (data.Value10 is not null && data.Value10.Length > 0)
{
for (int i = 0; i < data.Value10.Length; i++)
{
sb.AppendSpanFormattable(data.Value10[i]);
}
}
return sb.ToString();
}
}
结果如下所示。

从上图的结果中,我们可以得出如下的结论。
- 使用
StringConcat是最慢的,这种方式是无论如何都不推荐的。 - 使用
StringBuilder要比使用StringConcat快6.5倍,这是推荐的方法。 - 设置了初始容量的
StringBuilder要比直接使用StringBuilder快25%,正如我在你应该为集合类型设置初始大小一样,设置初始大小绝对是相当推荐的做法。 - 栈上分配的
ValueStringBuilder比StringBuilder要快50%,比设置了初始容量的StringBuilder还快25%,另外它的GC次数是最低的。 - 堆上分配的
ValueStringBuilder比StringBuilder要快55%,他的GC次数稍高与栈上分配。
从上面的结论中,我们可以发现ValueStringBuilder的性能非常好,就算是在栈上分配缓冲区,性能也比StringBuilder快25%。
源码解析
ValueStringBuilder的源码不长,我们挑几个重要的方法给大家分享一下,部分源码如下。
// 使用 ref struct 该对象只能在栈上分配
public ref struct ValueStringBuilder
{
// 如果从ArrayPool里分配buffer 那么需要存储一下
// 以便在Dispose时归还
private char[]? _arrayToReturnToPool;
// 暂存外部传入的buffer
private Span<char> _chars;
// 当前字符串长度
private int _pos;
// 外部传入buffer
public ValueStringBuilder(Span<char> initialBuffer)
{
// 使用外部传入的buffer就不使用从pool里面读取的了
_arrayToReturnToPool = null;
_chars = initialBuffer;
_pos = 0;
}
public ValueStringBuilder(int initialCapacity)
{
// 如果外部传入了capacity 那么从ArrayPool里面获取
_arrayToReturnToPool = ArrayPool<char>.Shared.Rent(initialCapacity);
_chars = _arrayToReturnToPool;
_pos = 0;
}
// 返回字符串的Length 由于Length可读可写
// 所以重复使用ValueStringBuilder只需将Length设置为0
public int Length
{
get => _pos;
set
{
Debug.Assert(value >= 0);
Debug.Assert(value <= _chars.Length);
_pos = value;
}
}
......
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Append(char c)
{
// 添加字符非常高效 直接设置到对应Span位置即可
int pos = _pos;
if ((uint) pos < (uint) _chars.Length)
{
_chars[pos] = c;
_pos = pos + 1;
}
else
{
// 如果buffer空间不足,那么会走
GrowAndAppend(c);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Append(string? s)
{
if (s == null)
{
return;
}
// 追加字符串也是一样的高效
int pos = _pos;
// 如果字符串长度为1 那么可以直接像追加字符一样
if (s.Length == 1 && (uint) pos < (uint) _chars .Length)
{
_chars[pos] = s[0];
_pos = pos + 1;
}
else
{
// 如果是多个字符 那么使用较慢的方法
AppendSlow(s);
}
}
private void AppendSlow(string s)
{
// 追加字符串 空间不够先扩容
// 然后使用Span复制 相当高效
int pos = _pos;
if (pos > _chars.Length - s.Length)
{
Grow(s.Length);
}
s
#if !NETCOREAPP
.AsSpan()
#endif
.CopyTo(_chars.Slice(pos));
_pos += s.Length;
}
// 对于需要格式化的对象特殊处理
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AppendSpanFormattable<T>(T value, string? format = null, IFormatProvider? provider = null)
where T : ISpanFormattable
{
// ISpanFormattable非常高效
if (value.TryFormat(_chars.Slice(_pos), out int charsWritten, format, provider))
{
_pos += charsWritten;
}
else
{
Append(value.ToString(format, provider));
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private void GrowAndAppend(char c)
{
// 单个字符扩容在添加
Grow(1);
Append(c);
}
// 扩容方法
[MethodImpl(MethodImplOptions.NoInlining)]
private void Grow(int additionalCapacityBeyondPos)
{
Debug.Assert(additionalCapacityBeyondPos > 0);
Debug.Assert(_pos > _chars.Length - additionalCapacityBeyondPos,
"Grow called incorrectly, no resize is needed.");
// 同样也是2倍扩容,默认从对象池中获取buffer
char[] poolArray = ArrayPool<char>.Shared.Rent((int) Math.Max((uint) (_pos + additionalCapacityBeyondPos),
(uint) _chars.Length * 2));
_chars.Slice(0, _pos).CopyTo(poolArray);
char[]? toReturn = _arrayToReturnToPool;
_chars = _arrayToReturnToPool = poolArray;
if (toReturn != null)
{
// 如果原本就是使用的对象池 那么必须归还
ArrayPool<char>.Shared.Return(toReturn);
}
}
//
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Dispose()
{
char[]? toReturn = _arrayToReturnToPool;
this = default; // 为了安全,在释放时置空当前对象
if (toReturn != null)
{
// 一定要记得归还对象池
ArrayPool<char>.Shared.Return(toReturn);
}
}
}
从上面的源码我们可以总结出ValueStringBuilder的几个特征:
- 比起
StringBuilder来说,实现方式非常简单。 - 一切都是为了高性能,比如各种
Span的用法,各种内联参数,以及使用对象池等等。 - 内存占用非常低,它本身就是结构体类型,另外它是
ref struct,意味着不会被装箱,不会在堆上分配。
适用场景
ValueStringBuilder是一种高性能的字符串创建方式,针对于不同的场景,可以有不同的使用方式。
1.非常高频次的字符串拼接的场景,并且字符串长度较小,此时可以使用栈上分配的ValueStringBuilder。
大家都知道现在ASP.NET Core性能非常好,在其依赖的内部库UrlBuilder中,就使用栈上分配,因为栈上分配在当前方法结束后内存就会回收,所以不会造成任何GC压力。

2.非常高频次的字符串拼接场景,但是字符串长度不可控,此时使用ArrayPool指定容量的ValueStringBuilder。比如在.NET BCL库中有很多场景使用,比如动态方法的ToString实现。从池中分配虽然没有栈上分配那么高效,但是一样的能降低内存占用和GC压力。

3. 非常高频次的字符串拼接场景,但是字符串长度可控,此时可以栈上分配和ArrayPool分配联合使用,比如正则表达式解析类中,如果字符串长度较小那么使用栈空间,较大那么使用ArrayPool。

需要注意的场景
1.在async\await中无法使用ValueStringBuilder。原因大家也都知道,因为ValueStringBuilder是ref struct,它只能在栈上分配,async\await会编译成状态机拆分await前后的方法,所以ValueStringBuilder不好在方法内传递,不过编译器也会警告。

2.无法将ValueStringBuilder作为返回值返回,因为在当前栈上分配,方法结束后它会被释放,返回它将指向未知的地址。这个编译器也会警告。

3.如果要将ValueStringBuilder传递给其它方法,那么必须使用ref传递,否则发生值拷贝会存在多个实例。这个编译器不会警告,但是你必须非常注意。

4. 如果使用栈上分配,那么Buffer大小控制在5KB内比较稳妥,至于为什么需要这样,后面有机会在讲一讲。
总结
今天和大家分享了一下高性能几乎无内存占用的字符串拼接结构体ValueStringBuilder,在大多数的场景还是推荐大家使用。但是要非常注意上面提到的的几个场景,如果不符合条件,那么大家还是可以使用高效的StringBuilder来进行字符串拼接。
本文源码链接: https://github.com/InCerryGit/BlogCode-Use-ValueStringBuilder
.NET性能优化-使用ValueStringBuilder拼接字符串的更多相关文章
- Mysql性能优化:如何给字符串加索引?
导读 现代大部分的登录系统都支持邮箱.手机号码登录两种方式,那么如何在邮箱或者手机号码这个字符串上建立索引才能保证性能最佳呢? 今天这篇文章就来探讨一下在Mysql中如何给一个字符串加索引才能达到性能 ...
- java 性能优化 字符串过滤实战
转自[http://www.apkbus.com/blog-822717-78335.html] 如有不妥联系删除!! ★一个简单的需求 首先描述一下需求:给定一个 String 对象,过滤掉除了数 ...
- AJPFX浅谈Java 性能优化之字符串过滤实战
★一个简单的需求 首先描述一下需求:给定一个 String 对象,过滤掉除了数字(字符'0'到'9')以外的其它字符.要求时间开销尽可能小.过滤函数的原型如下: String filter(Strin ...
- java性能优化--字符串优化处理
String对象 String对象是java中重要的数据类型,在大部分情况下我们都会用到String对象.其实在Java语言中,其设计者也对String做了大量的优化工作,这些也是String对象的特 ...
- String字符串性能优化的几种方案
String字符串是系统里最常用的类型之一,在系统中占据了很大的内存,因此,高效地使用字符串,对系统的性能有较好的提升. 针对字符串的优化,我在工作与学习过程总结了以下三种方案作分享: 一.优化构建的 ...
- String字符串性能优化的探究
一.背景 String 对象是我们使用最频繁的一个对象类型,但它的性能问题却是最容易被忽略的.String 对象作为 Java 语言中重要的数据类型,是内存中占用空间最大的一个对象,高效地使用字符串, ...
- Java字符串之性能优化
基础类型转化成String 在程序中你可能时常会需要将别的类型转化成String,有时候可能是一些基础类型的值.在拼接字符串的时候,如果你有两个或者多个基础类型的值需要放到前面,你需要显式的将第一个值 ...
- Java性能优化之String字符串优化
字符串是软件开发中最重要的对象之一.通常,字符串对象在内存中是占据了最大的空间块,因此如何高效地处理字符串,必将是提高整体性能的关键所在. 1.字符串对象及其特点 Java中八大基本数据类型没有Str ...
- Sql动态查询拼接字符串的优化
Sql动态查询拼接字符串的优化 最原始的 直接写:string sql="select * from TestTables where 1=1";... 这样的代码效率很低的,这样 ...
随机推荐
- 解释 Java 堆空间及 GC?
当通过 Java 命令启动 Java 进程的时候,会为它分配内存.内存的一部分用于 创建堆空间,当程序中创建对象的时候,就从对空间中分配内存.GC 是 JVM 内 部的一个进程,回收无效对象的内存用于 ...
- Java 中,编写多线程程序的时候你会遵循哪些最佳实践?
这是我在写 Java 并发程序的时候遵循的一些最佳实践: a)给线程命名,这样可以帮助调试. b)最小化同步的范围,而不是将整个方法同步,只对关键部分做同步. c)如果可以,更偏向于使用 volati ...
- Python学习--21天Python基础学习之旅(Day08-Day21)
Day08: Chapter 11 异常 1.1如果try代码块中代码运行没有问题,则运行时跳过except代码块.(有异常时执行except中的代码,没有异常则跳过except中的代码) try-e ...
- 7. Github Pages 搭建网站
7. Github Pages 搭建网站 个人站点 访问 https://用户名.github.io 搭建步骤 1) 创建个人站点 -> 新建仓库(注:仓库名必须是[用户名.github. ...
- Pycharm使用 Ctrl+滚轮 调整字体大小
首先,打开File中的Settings 然后,点开Editor内的General 最后,在3 指向的位置勾选:Change font size (Zoom)with Ctrl+Mouse Whel 这 ...
- 二、cadence焊盘与封装制作操作步骤详细说明
一.焊盘制作 1.打开Pad Designer软件,新建文件--设置保存路径和焊盘名称(规范命名) 2.Parameters--设置单位--过孔类型--是否镀金 3.Layers--single la ...
- php弹窗后跳入另一个页面
之前写项目时,在跳转页面前加入一个弹窗,发现弹窗没有弹出来就直接跳转了,之前使用的header跳转发现不行,换成location.href也不行,后来再location.href前加入一个parent ...
- ruby 版本管理RVM (ruby version manager)
macOS. 自带的ruby 版本目录权限比较高, 经常有很多 操作需要权限而不能执行 虽然 macOS 自带了一个 ruby 环境,但是是系统自己使用的,所以权限很小,只有 system. 而/Li ...
- LeetCode 剑指 Offer 22. 链表中倒数第k个节点
剑指 Offer 22. 链表中倒数第k个节点 题意 输入一个链表,输出该链表中倒数第k个节点.为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点. 例如,一个链表有 6 个 ...
- Element instanceof Node
今天看到一个问题,问 Element instance Node 为什么是 false. 首先,我们知道 Element 是 Node 的子类,那么为什么 Element instanceof Nod ...