StringBuilder内存碎片对性能的影响
StringBuilder内存碎片对性能的影响
TL;DR:
StringBuilder内部是由多段char[]组成的半自动链表,因此频繁从中间修改StringBuilder,会将原本连续的内存分隔为多段,从而影响读取/遍历性能。
连续内存与不连续内存的性能差,可能高达1600倍。
背景
用StringBuilder的用户可能大都想用StringBuilder拼接html/json模板、组装动态SQL等正常操作。但在一些特殊场景中——如为某种编程语言写语言服务,或者写一个富文本编辑器时,StringBuilder依然也有用武之地,通过里面的Insert/Remove两个方法来修改。
测试方法
Talk is cheap, show me the code:
int docLength = 10000;
void Main()
{
(from power in Enumerable.Range (1, 16)
let mutations = (int) Math.Pow (2, power)
select new
{
mutations,
PerformanceRatio = Math.Round (GetPerformanceRatio (docLength, mutations), 1)
}).Dump();
}
float GetPerformanceRatio (int docLength, int mutations)
{
var sb = new StringBuilder ("".PadRight (docLength));
var before = GetPerformance (sb);
FragmentStringBuilder (sb, mutations);
var after = GetPerformance (sb);
return (float) after.Ticks / before.Ticks;
}
void FragmentStringBuilder (StringBuilder sb, int mutations)
{
var r = new Random(42);
for (int i = 0; i < mutations; i++)
{
sb.Insert (r.Next (sb.Length), 'x');
sb.Remove (r.Next (sb.Length), 1);
}
}
TimeSpan GetPerformance (StringBuilder sb)
{
var sw = Stopwatch.StartNew();
long tot = 0;
for (int i = 0; i < sb.Length; i++)
{
char c = sb[i];
tot += (int) c;
}
sw.Stop();
return sw.Elapsed;
}
关于这段代码,请注意以下几点:
- 通过
.PadRight(n)来直接创建长度为n的空白字符串,可以用new string(' ', n)来代替; new Random(42)处,我指定了一个随机因子,确保每次分隔后分隔的位置完全相同,有利于做对照组;- 我分别对字符串进行了
2^1 ~ 2^16次修改,分别比较经过这么多次修改之后的性能差异; - 我使用
sb[i]来逐一访问StringBuilder中的位置,使内存不连续性更加突显。
运行结果
| mutations | PerformanceRatio |
|---|---|
| 2 | 1 |
| 4 | 1 |
| 8 | 1 |
| 16 | 1 |
| 32 | 1 |
| 64 | 1.1 |
| 128 | 1.2 |
| 256 | 1.8 |
| 512 | 5.2 |
| 1024 | 19.9 |
| 2048 | 81.3 |
| 4096 | 274.5 |
| 8192 | 745.8 |
| 16384 | 1578.8 |
| 32768 | 1630.4 |
| 65536 | 930.8 |
可见如果在StringBuilder中间进行大量修改,其性能会急据下降,注意看32768次修改的情况下,遍历时会产生高达1630.4倍的性能差!
解决方式
如果一定要用StringBuilder,可以考虑在修改一定次数后,重新创建一个新的StringBuilder,以使得访问时获得最佳的内存连续性,即可解决此问题:
void FragmentStringBuilder (StringBuilder sb, int mutations)
{
var r = new Random(42);
for (int i = 0; i < mutations; i++)
{
sb.Insert (r.Next (sb.Length), 'x');
sb.Remove (r.Next (sb.Length), 1);
// 重点
const int defragmentCount = 250;
if (i % defragmentCount == defragmentCount - 1)
{
string buf = sb.ToString();
sb.Clear();
sb.Append(buf);
}
}
}
如上,每经过250次修改,即将原StringBuilder删除,然后重新创建一个新的StringBuilder,此时运行效果如下:
| mutations | PerformanceRatio |
|---|---|
| 2 | 1.2 |
| 4 | 0.7 |
| 8 | 1 |
| 16 | 1 |
| 32 | 1 |
| 64 | 1.1 |
| 128 | 1.2 |
| 256 | 1 |
| 512 | 1 |
| 1024 | 1 |
| 2048 | 1 |
| 4096 | 1.1 |
| 8192 | 1.5 |
| 16384 | 1.3 |
| 32768 | 1 |
| 65536 | 1 |
可见,在几乎所有情况下,受内存不连续造成的访问性能问题,解决——同时250可能是一个相对比较合理的数字,在插入性能与查询/遍历性能中,获得平衡。
反思与总结
众所周知,由于string的不可变性,拼接大量字符串时,会浪费大量内存。但使用StringBuilder也需要了解它的结构。
StringBuilder这样做成链式的结构并非没有原因,如果考虑插入性能,做成链式接口是最优秀的。但如果考虑查询性能,链式结构就非常不利了,如果设计为非链式结构,从中间插入时,StringBuilder的内存空间可能不够,因此需要重新分配内存,这样相当于将StringBuilder降格为string,因此完全丧失了StringBuilder适合做“频繁插入”的优势。
本文说的其实是一个非常特殊的例子,现实中除了语言服务、编辑器外,很少会需要这种即要频繁插入快,也要频繁修改快的场景。如果想简单点搞,用StringBuilder会是一个有条件合适的解决方案。更适合的解决方案当然是专门的数据结构——PieceTable,微软在VSCode编辑器中,为了确保大文件编辑性能,使用了该数据结构,取得了非常不错的成果,参考链接:Text Buffer Reimplementation。
喜欢的朋友请关注我的微信公众号:【DotNet骚操作】

StringBuilder内存碎片对性能的影响的更多相关文章
- 2019-8-31-C#-程序集数量对软件启动性能的影响
title author date CreateTime categories C# 程序集数量对软件启动性能的影响 lindexi 2019-08-31 16:55:58 +0800 2018-10 ...
- 高性能JavaScript-JS脚本加载与执行对性能的影响
在web产品优化准则中,很重要的一条是针对js脚本的加载和执行方式的优化.本篇文章简单描述一下其中的优化准则. 1. 脚本加载优化 1.1 脚本位置对性能的影响 优化页面加载性能的原则之一是将scri ...
- JAVA 异常对于性能的影响
陶炳哲 - MAY 12, 2015 在对OneAPM的客户做技术支持时,我们常常会看到很多客户根本没意识到的异常.在消除了这些异常之后,代码运行速度与以前相比大幅提升.这让我们产生一种猜测,就是在代 ...
- HTTP/2 对 Web 性能的影响(下)
一.前言 我们在 HTTP/2 对 Web 性能的影响(上)已经和大家分享了一些关于 Http2 的二项制帧.多用复路以及 APM 工具等,本文作为姊妹篇,主要从 http2 对 Web 性能的影响. ...
- smarty对网页性能的影响--开启opcache
在上一篇<smarty对网页性能的影响>中,默认没有开启opcache,于是我安装了一下zend opcache扩展,重新实验了一下,结果如下: 有smarty 用apache的ab命令进 ...
- C++ 性能剖析 (四):Inheritance 对性能的影响
(这个editor今天有毛病,把我的format全搞乱了,抱歉!) Inheritance 是OOP 的一个重要特征.虽然业界有许多同行不喜欢inheritance,但是正确地使用inheritanc ...
- css的!important规则对性能有影响吗
最近在做项目中发现很多CSS代码里面都使用!important去覆盖原有高优先级的样式.按照常理来说,越是灵活的东西,需要做的工作就会更多.所以想当然的认为像!important这样灵活.方便的规则如 ...
- List是线程安全的吗?如果不是该怎么办呢?安全的List对性能的影响有多大呢?
测试条件: 开启2个并行执行任务,往同一个list对象写入值 测试代码: ; static List<int> list = new List<int>(); static v ...
- JS脚本加载与执行对性能的影响
高性能JavaScript-JS脚本加载与执行对性能的影响 在web产品优化准则中,很重要的一条是针对js脚本的加载和执行方式的优化.本篇文章简单描述一下其中的优化准则. 1. 脚本加载优化 1.1 ...
随机推荐
- RSA算法原理(简单易懂)
1. 什么是RSA RSA算法是现今使用最广泛的公钥密码算法,也是号称地球上最安全的加密算法.在了解RSA算法之前,先熟悉下几个术语 根据密钥的使用方法,可以将密码分为对称密码和公钥密码 对称密码:加 ...
- jenkins使用(1)
术语:构建一次job指的是执行一次任务 注:到了公司,如果需要搭建jenkins环境可以找运维 jenkins使用: 创建视图 常用的两个配置: 新建任务: 可以选择构建后的步骤: 然后保存 图标状态 ...
- Java程序、JSP以及JavaScript中如何判断某个字符串是否包含某个子串
1.JSP str:原始字符串, subStr:要查找的子字符串 <c:if test="${fn:contains(str,subStr)==true}"> < ...
- 吴裕雄--天生自然python编程:实例(2)
list1 = [10, 20, 4, 45, 99] list1.sort() print("最小元素为:", *list1[:1]) list1 = [10, 20, 1, 4 ...
- from PIL import image报错
python中import PIL可以,但是from PIL import Image就报错? ’‘ 大家在安装pillow的时候,可能会安装成功,但是当运行from pIL import image ...
- Maven基本概念和操作
最近在学Java,找来一个开源项目练手,它是用 Spring Boot 搭建的框架,于是去学 Spring Boot,然而 Spring Boot 需要有 Spring 框架和 Maven 的使用经验 ...
- String.slice
String.slice(start, end)start从字符串的哪个index开始截取 默认值0 如果为负值,则从字符串的尾部向前倒推indexend到从字符串的哪个index结束截取 默认值st ...
- Html的label和span的区别
从最终效果来看,label与span标签显示方式及作用都一样的但由于label中有for属性的存在,也有着决定性的不同 for属性将label和表单进行配对 label标签通常是写在表单(form)内 ...
- zookeeper 实战 - Pymjer 的博客
下载 $ wget http://apache.forsale.plus/zookeeper/zookeeper-3.4.9/zookeeper-3.4.9.tar.gz 配置conf/zoo.cfg ...
- 一起了解 .Net Foundation 项目 No.11
.Net 基金会中包含有很多优秀的项目,今天就和笔者一起了解一下其中的一些优秀作品吧. 中文介绍 中文介绍内容翻译自英文介绍,主要采用意译.如与原文存在出入,请以原文为准. Microsoft Web ...