您的位置:首页 > 其它

重谈字符串连接性能(下):分析优化

2009-12-23 14:03 423 查看
经过之间的性能比较,我们得知StringBuilder的性能并非时时最优,再经过实现分析,我们大致了解了StringBuilder的实现方式。虽然在此之前,大家也基本已经了解StringBuilder的实现原理,也有不少朋友指出了它性能缺陷的原因。不过“严谨”起见,寻找性能问题的方式应该是进行Profiling,然后找出性能关键再进行优化——而不是纯粹进行“阅读”这种静态分析方式。

那么,假设我们还是使用原来的方式使用StringBuilder连接字符串:

static void Main()
{
for (int i = 0; i < 100 * 100 * 20; i++) StringBuilder(1024);
}

private static readonly string STR = "0123456789";

private static string StringBuilder(int count)
{
var builder = new StringBuilder();
for (int i = 0; i < count; i++)
builder.Append(STR);
return builder.ToString();
}


我们对这段代码进行Profiling,便可以得到这样的结果:



从结果上可以看出,几乎所有时间都是消耗在Append操作上的(这是废话)。而在Append方法中,AppendInPlace和GetNewString方法都占用了较多的比例。从上次的代码分析中我们知道,AppendInPlace方法是将新的字符串复制到原字符序列(也就是那个“容器”)的后面,而GetNewString的作用便是创建一个新的,容量加倍字符串(容器)——它的主要消耗都在GetStringForStringBuilder方法上。

AppendInPlace的作用是复制字符串,消耗无法节省下来。但是,我们可以尽可能避免GetNewString的开销,只要减少“创建新容器”的次数即可。这意味着我们可以在一开始指定容量更大的StringBuilder。于是乎,我们尝试将StringBuilder的使用改写为如下形式:

private static string NewStringBuilder(int count)
{
var builder = new StringBuilder(count * STR.Length);
for (int i = 0; i < count; i++)
builder.Append(STR);
return builder.ToString();
}


再次进行Profiling,结果如下:



由于我们一下子提供了足够的容量,因此在NewStringBuilder方法中一次“扩容”都不需要,因此也就不会调用GetNewString方法了。从上图中可以看出,此时AppendInPlace方法占用的比例增加了。与此对应的是StringBuilder的构造函数开销也增大了——因为需要一下子开辟较多的空间。由于总时间消耗地少,因此采样总数也比之前有所减少——这些结果都符合我们的推测。

于是我们将NewStringBuilder和之前的StringConcat以及StringListBuilder进行比较。公平起见,我也相应提高StringListBuilder中List<string>的容量,避免“扩容操作”:

class Program
{
static void Main()
{
CodeTimer.Initialize();

for (int i = 2; i <= 4096; i *= 2)
{
CodeTimer.Time(
String.Format("StringBuilder ({0})", i),
10000,
() => NewStringBuilder(i));

CodeTimer.Time(
String.Format("String.Concat ({0})", i),
10000,
() => StringConcat(i));

CodeTimer.Time(
String.Format("StringListBuilder ({0})", i),
10000,
() => StringListBuilder(i));
}
}

private static readonly string STR = "0123456789";

private static string NewStringBuilder(int count) { var builder = new StringBuilder(count * STR.Length); for (int i = 0; i < count; i++) builder.Append(STR); return builder.ToString(); }

private static string StringConcat(int count)
{
var array = new string[count];
for (int i = 0; i < count; i++) array[i] = STR;
return String.Concat(array);
}

private static string StringListBuilder(int count)
{
var builder = new StringListBuilder(count);
for (int i = 0; i < count; i++) builder.Append(STR);
return builder.ToString();
}
}

public class StringListBuilder
{
private List<string> m_list;

public StringListBuilder(int capacity)
{
this.m_list = new List<string>(capacity);
}

public StringListBuilder Append(string s)
{
this.m_list.Add(s);
return this;
}

public string ToString()
{
return String.Concat(this.m_list.ToArray());
}
}


结果如下:

绘制成图表:



终于,StringBuilder翻身了。由于避免了不断扩容,不断复制的过程,因此StringBuilder的性能已经成为三者中性能最高的作法。事实上,String.Concat高性能的原因,也正是事先知道了目标字符串的长度,实现了最高效的构造方法。而StringListBuilder,它比String.Concat需要进行更多List<string>和数组方面的维护,因此性能略低一些。

那么,经过了这三篇文章的比较和分析之后,我们是否可以知道哪种字符串连接方式性能最高呢?自然这需要根据情况而定:

StringBuilder:如果能够确定目标字符串的最终长度,则可以使用StringBuilder。如果不能确定的话,也可以在一开始指定更大的容量,减少扩容的次数。

String.Concat:如果不能确定最终长度,但是能够确定字符串的个数(如这个场景),可以将它们放在一个数组中,并调用String.Concat进行连接。

StringListBuilder:折衷方案,与String.Concat相比其优势在于无需确定字符串个数,与StringBuilder相比其优势在于“扩容”操作只需复制一些引用即可。

我的“重谈”之旅到此就告一段落了,您是否觉得哪里还意犹未尽呢?

相关文章

重谈字符串连接性能(上):性能评测

重谈字符串连接性能(中):细节实现

重谈字符串连接性能(下):分析优化
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐