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 ...
随机推荐
- iphone开发学习之路--基本语法
关键字:Objective-C(以下简称O-C)是C语言的一个超集,也就是C语言的语法O-C都是兼容的,所以为了避免冲突O-C的关键字都是以@符号开始的,比如:@class.@public .@try ...
- SpringMVC中Interceptor和Filter区别
Interceptor 主要作用:拦截用户请求,进行处理,比如判断用户登录情况,权限验证,主要针对Action请求进行处理.是通过HandlerInterceptor 实现的. 配置如下: <m ...
- keepalive笔记之二:keepalive+nginx(自定义脚本实现,上述例子也可以实现)
keepalive的配置文件 ! Configuration File for keepalived global_defs { notification_email { acassen@firewa ...
- Nginx笔记总结八:ngx_http_core_module模块中的变量
$arg_patameter HTTP请求中某个参数的值,如/index.php?site=www.ttlsa.com,可以用$arg_site取得www.ttlsa.com这个值 $args HTT ...
- Nginx笔记总结六:Nginx location配置
语法规则:location [= | ~ | ~* | ^~] /uri/ {....} = 表示精确匹配 ^~ 表示uri以某个常规字符串开头 ~ 表示区分大小写的正则表达式 ~* 表示不区分大小写 ...
- echarts柱状图宽度设置(react-native)
const optionCategory = { color: ['#B5282A'], tooltip : { trigger: 'axis', axisPointer : { // 坐标轴指示器, ...
- GLPI 0.85.5 上传漏洞分析
在exp-db上面看到的漏洞,这是原文链接:https://www.exploit-db.com/exploits/38407/ 但是POC给的很简单,这是原来的描述: " The appl ...
- 安卓权威编程指南-笔记(第27章 broadcast intent)
本章需求:首先,让应用轮询新结果并在有所发现时及时通知用户,即使用户重启设备后还没有打开过应用.其次,保证用户在使用应用时不出现新结果通知. 1. 一般intent和broadcast intent ...
- Alberto Del Bimbo:为什么说研究员要有想象力?
Del Bimbo:为什么说研究员要有想象力?" title="Alberto Del Bimbo:为什么说研究员要有想象力?"> 说到科研,与日本式的&q ...
- 软工 实验一 Git代码版本管理
实验目的: 1)了解分布式版本控制系统的核心机理: 2) 熟练掌握git的基本指令和分支管理指令: 实验内容: 1)安装git 2)初始配置git ,git init git status指令 3 ...