ICode9

精准搜索请尝试: 精确搜索
首页 > 系统相关> 文章详细

StringBuilder内存碎片对性能的影响

2020-03-21 18:58:14  阅读:241  来源: 互联网

标签:int StringBuilder mutations 碎片 内存 new sb


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;
}

关于这段代码,请注意以下几点:

  1. 通过.PadRight(n)来直接创建长度为n的空白字符串,可以用new string(' ', n)来代替;
  2. new Random(42)处,我指定了一个随机因子,确保每次分隔后分隔的位置完全相同,有利于做对照组;
  3. 我分别对字符串进行了2^1 ~ 2^16次修改,分别比较经过这么多次修改之后的性能差异;
  4. 我使用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骚操作】

DotNet骚操作

标签:int,StringBuilder,mutations,碎片,内存,new,sb
来源: https://www.cnblogs.com/sdflysha/p/20200317-memory-fragment-performance-impact-to-stringbu

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有