在平时的开发中难免会遇到字符串拼接的情况。比较常用的方法有:StringBuilder,+运算符,string.Format和string.Concat。
在.NET程序员中一直流传着一个传说:StringBuilder的性能可以吊打+运算符。不知道大家有没有亲自测试过这个传说,反正我以前没有没测试过。
通过查看源代码可以发现string.Format是通过调用StringBuilder中的方法来实现字符串拼接的。而且相对来说string.Format对字符串的格式很友好。所以在平时的开发中我最常用的方法就是string.Format。这样不仅字符串的格式看起来美美哒而且速度还快。啧啧啧,我简直就是智慧与美貌并存的程序员呢。我都忍不住为自己点上32个赞。直到有一天有人告诉我,string.Format的速度比直接用+要慢多了。我当然是不信啊,所以决定简单的测试一下。
废话不多说,直接上代码。
Program类:
static void Main(string[] args) { TimeSpan span_Format = new TimeSpan(); TimeSpan span_PlusSign = new TimeSpan(); TimeSpan span_Concat = new TimeSpan(); TimeSpan span_StringBuilderAppend = new TimeSpan(); for (int i = 0; i < 10; i++) { span_Format += StringFormat(); span_PlusSign += StringPlusSign(); span_Concat += StringConcat(); span_StringBuilderAppend += StringBuilderAppend(); Console.Write(Environment.NewLine); GC.Collect(3); } Console.WriteLine("StringFormat:{0}", span_Format); Console.WriteLine("StringPlusSign:{0}", span_PlusSign); Console.WriteLine("StringConcat:{0}", span_Concat); Console.WriteLine("StringBuilderAppend:{0}", span_StringBuilderAppend); Console.WriteLine("over"); Console.ReadKey(); } private static TimeSpan StringFormat() { var source = GetStringList(); var t = RunTime.GetMethodRunTime(() => { string s = string.Empty; foreach (var item in source) { s = string.Format("{0}{1}", s, item); } }); Console.WriteLine("string.Format:" + t); return t; } private static TimeSpan StringPlusSign() { var source = GetStringList(); var t = RunTime.GetMethodRunTime(() => { string s = string.Empty; foreach (var item in source) { s = s + item; } }); Console.WriteLine("string.+:" + t); return t; } private static TimeSpan StringConcat() { var source = GetStringList(); var t = RunTime.GetMethodRunTime(() => { string s = string.Empty; s = string.Concat(source.ToArray()); }); Console.WriteLine("string.Concat:" + t); return t; } private static TimeSpan StringBuilderAppend() { var source = GetStringList(); var t = RunTime.GetMethodRunTime(() => { StringBuilder builder = new StringBuilder(); foreach (var item in source) { builder.Append(item); } builder.ToString(); }); Console.WriteLine("StringBuilder:" + t); return t; } private static List<string> GetStringList() { List<string> list = new List<string>(10000); for (int i = 0; i < 10000; i++) { StringBuilder builder = new StringBuilder(); builder.AppendFormat("回" + i); builder.AppendFormat("龙" + i); builder.AppendFormat("观" + i); builder.AppendFormat("吴" + i); builder.AppendFormat("彦" + i); builder.AppendFormat("祖" + i); list.Add(builder.ToString()); } return list; }
RunTime类:
public static TimeSpan GetMethodRunTime(Action action) { Stopwatch watch = new Stopwatch(); watch.Start(); action(); watch.Stop(); return watch.Elapsed; }
代码的结构很简单,在Program类中的GetStringList方法是用来产生一个一万个字符串的List作为数据源。StringFormat,StringPlusSign,StringConcat,StringBuilderAppend这四个方法是用来计算不同的方法连接字符串所用的时间。RunTime类中的GetMethodRunTime方法主要是用来计算传入方法的运行时间。在Main函数中每次将这四个方法每个都运行10次,每次运行结束后都强制进行一次垃圾回收。最后计算每个方法运行十次后的总时间。
最后的运行结果如下:
通过测试我们可以知道,StringBuilder的速度确实可以吊打+运算符,但是和string.Concat方法之间差距很小(在我调试的过程中,这两种连接方式的速度并不固定有的时候是string.Concat耗时少有的时候是StringBuilder耗时少),string.Format的速度确实很慢。拼接字符串的时候想要保持格式的优雅是需要在速度上付出代价的。不知道自己以前制造的那些垃圾代码现是不是已经成了性能瓶颈。
作为一个程序员,当然不会到此为止。接下来我们来研究下为什么不同的方法之间为什么会有大的差距!
首先是速度最快的StringBuilder.Append和string.Concat方法。关于StringBuilder.Append和string.Concat方法的实现,建议阅读老赵的三篇文章,讲的深入浅出鞭辟入里(老赵的文章链接我放在文末了)。
在这里把老赵三篇文章里最后分析出的和本文有关的结果总结下:
Concat方法会先根据传递过来的数组参数计算出要组合的字符串的最终长度,然后在内存中分配同样大的一段空间。然后调用一段非安全的代码直接操作内存将,参数依次填充到这段内存中。
StringBuilder的Append方法则是动态分配内存。如果发现string对象的容量不够了会线程安全的重新分配一段更大的内存空间,然后继续在末尾追加字符串。
所以虽然StringBuilder.Append和string.Concat耗时差不多,但是从理论上来看,如果连接的字符串比较多的情况下,string.Concat的速度应该是比StringBuilder.Append更快的
然后是速度较慢的+运算符。反编译程序之后可以发现tring中+的加号被编译成了string.Concat。(这里可能不太准确。到底是编译的时候CLR将+编译成了string.Concat方法还是String类重载了+运算符,这点存在疑问。但是在String类源码的时候没有发现+运算符的重载,所以我推断是在编译时把+编译成了string.Concat方法)。这里需要特别提一点,同样都是string.Concat为什么在Demo中+运算符的速度会比直接用string.Concat慢这么多。这主要是因为通过+连接的时候每次只能传两个参数需要循环多次才能把所有的字符串都拼接在一起,而Concat则是一次把所有的参数都传过去。
+
Concat
最后是速度慢到令人发指的string.Format。通过查看Framework的源代码追根溯源我们可以知道string.Forma最终调用的是StringBuilder的AppendFormatHelper方法。虽然都是StringBuilder中的方法,但是Append和AppendFormatHelper是截然不同的两个方法。在AppendFormatHelper方法中需要依次循环每个字符来来判断‘{’和‘}’以及数字占位符的位置。这是AppendFormatHelper方法执行效率低的主要原因。
通过对以上几种连接方法的对比,可以知道,要想优雅是要付出代价的。就像我,虽然我的颜值冠绝回龙观所有的程序员,但是我的编程水平却很菜。
老赵分析字符串连接性能的文章
http://www.cnblogs.com/JeffreyZhao/archive/2009/11/26/string-concat-perf-1-benchmark.html
http://www.cnblogs.com/JeffreyZhao/archive/2009/12/23/string-concat-perf-3-profiling-analysis.html
(由于本文作者水平有限,文中难免有误,希望各位读者可以不吝赐教,在评论区中多多交流)