String.Join与StringBuilder:哪个更快?
在之前关于将double[][]
格式化为CSV格式的问题中,Marc Gravell 表示使用StringBuilder
将比String.Join
更快。 这是真的?
简短的回答:这取决于。
长答案: 如果你已经有一个string数组连接在一起(用分隔符), String.Join
是最快的方法。
String.Join
可以查看所有的string,找出所需的确切长度,然后再次复制所有的数据。 这意味着将不会有额外的复制。 唯一的缺点是它必须经过两次string,这意味着可能会使内存caching超出必要的时间。
如果事先没有将string作为数组,那么使用StringBuilder
可能会更快 – 但是会出现这种情况。 如果使用一个StringBuilder
意味着做大量的副本,然后build立一个数组,然后调用String.Join
可能会更快。
编辑:这是根据一个单一的调用String.Join
与一堆调用StringBuilder.Append
。 在原来的问题中,我们有两个不同级别的String.Join
调用,所以每个嵌套调用都会创build一个中间string。 换句话说,猜测更复杂也更困难。 我会惊讶地发现,无论哪种方式都会在典型的数据上“显着”(复杂性地)取胜。
编辑:当我在家的时候,我会写一个基准,这对于StringBuilder
可能是很痛苦的。 基本上,如果你有一个数组,其中每个元素的大小是前一个元素的两倍大小,并且你知道它是正确的,那么你应该能够强制每一个元素的附加元素,而不是分隔符,尽pipe这需要也要考虑到)。 在这一点上,它几乎和简单的string连接一样糟糕,但是String.Join
没有问题。
这里是我的testing平台,为简单起见使用int[][]
; 结果第一:
Join: 9420ms (chk: 210710000 OneBuilder: 9021ms (chk: 210710000
(更新double
结果:)
Join: 11635ms (chk: 210710000 OneBuilder: 11385ms (chk: 210710000
(更新重新2048 * 64 * 150)
Join: 11620ms (chk: 206409600 OneBuilder: 11132ms (chk: 206409600
并启用OptimizeForTesting:
Join: 11180ms (chk: 206409600 OneBuilder: 10784ms (chk: 206409600
这么快,但不是那么大; 钻机(在控制台运行,在发布模式等):
using System; using System.Collections.Generic; using System.Diagnostics; using System.Text; namespace ConsoleApplication2 { class Program { static void Collect() { GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); GC.WaitForPendingFinalizers(); GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced); GC.WaitForPendingFinalizers(); } static void Main(string[] args) { const int ROWS = 500, COLS = 20, LOOPS = 2000; int[][] data = new int[ROWS][]; Random rand = new Random(123456); for (int row = 0; row < ROWS; row++) { int[] cells = new int[COLS]; for (int col = 0; col < COLS; col++) { cells[col] = rand.Next(); } data[row] = cells; } Collect(); int chksum = 0; Stopwatch watch = Stopwatch.StartNew(); for (int i = 0; i < LOOPS; i++) { chksum += Join(data).Length; } watch.Stop(); Console.WriteLine("Join: {0}ms (chk: {1}", watch.ElapsedMilliseconds, chksum); Collect(); chksum = 0; watch = Stopwatch.StartNew(); for (int i = 0; i < LOOPS; i++) { chksum += OneBuilder(data).Length; } watch.Stop(); Console.WriteLine("OneBuilder: {0}ms (chk: {1}", watch.ElapsedMilliseconds, chksum); Console.WriteLine("done"); Console.ReadLine(); } public static string Join(int[][] array) { return String.Join(Environment.NewLine, Array.ConvertAll(array, row => String.Join(",", Array.ConvertAll(row, x => x.ToString())))); } public static string OneBuilder(IEnumerable<int[]> source) { StringBuilder sb = new StringBuilder(); bool firstRow = true; foreach (var row in source) { if (firstRow) { firstRow = false; } else { sb.AppendLine(); } if (row.Length > 0) { sb.Append(row[0]); for (int i = 1; i < row.Length; i++) { sb.Append(',').Append(row[i]); } } } return sb.ToString(); } } }
我不这么认为。 通过reflection器看, String.Join
的实现看起来非常优化。 它还具有了解总数的额外好处
预先创build的string大小,所以不需要重新分配。
我创build了两个testing方法来比较它们:
public static string TestStringJoin(double[][] array) { return String.Join(Environment.NewLine, Array.ConvertAll(array, row => String.Join(",", Array.ConvertAll(row, x => x.ToString())))); } public static string TestStringBuilder(double[][] source) { // based on Marc Gravell's code StringBuilder sb = new StringBuilder(); foreach (var row in source) { if (row.Length > 0) { sb.Append(row[0]); for (int i = 1; i < row.Length; i++) { sb.Append(',').Append(row[i]); } } } return sb.ToString(); }
我跑了每个方法50次,通过一个大小的数组[2048][64]
。 我为两个数组做了这个; 一个充满了零,另一个充满了随机
值。 我在我的机器上得到了以下结果(P4 3.0 GHz,单核,无HT,从CMD运行释放模式):
// with zeros: TestStringJoin took 00:00:02.2755280 TestStringBuilder took 00:00:02.3536041 // with random values: TestStringJoin took 00:00:05.6412147 TestStringBuilder took 00:00:05.8394650
将数组的大小增加到[2048][512]
,同时将迭代次数减less到10次,结果如下:
// with zeros: TestStringJoin took 00:00:03.7146628 TestStringBuilder took 00:00:03.8886978 // with random values: TestStringJoin took 00:00:09.4991765 TestStringBuilder took 00:00:09.3033365
结果是可重复的(几乎是由不同随机值引起的小波动)。 显然String.Join
大部分时间是稍微快一点(虽然是非常小的)。
这是我用来testing的代码:
const int Iterations = 50; const int Rows = 2048; const int Cols = 64; // 512 static void Main() { OptimizeForTesting(); // set process priority to RealTime // test 1: zeros double[][] array = new double[Rows][]; for (int i = 0; i < array.Length; ++i) array[i] = new double[Cols]; CompareMethods(array); // test 2: random values Random random = new Random(); double[] template = new double[Cols]; for (int i = 0; i < template.Length; ++i) template[i] = random.NextDouble(); for (int i = 0; i < array.Length; ++i) array[i] = template; CompareMethods(array); } static void CompareMethods(double[][] array) { Stopwatch stopwatch = Stopwatch.StartNew(); for (int i = 0; i < Iterations; ++i) TestStringJoin(array); stopwatch.Stop(); Console.WriteLine("TestStringJoin took " + stopwatch.Elapsed); stopwatch.Reset(); stopwatch.Start(); for (int i = 0; i < Iterations; ++i) TestStringBuilder(array); stopwatch.Stop(); Console.WriteLine("TestStringBuilder took " + stopwatch.Elapsed); } static void OptimizeForTesting() { Thread.CurrentThread.Priority = ThreadPriority.Highest; Process currentProcess = Process.GetCurrentProcess(); currentProcess.PriorityClass = ProcessPriorityClass.RealTime; if (Environment.ProcessorCount > 1) { // use last core only currentProcess.ProcessorAffinity = new IntPtr(1 << (Environment.ProcessorCount - 1)); } }
除非1%的差异在整个程序运行的时间上变成重要的东西,否则这看起来就像是微观优化。 我写的代码是最可读/可理解的,不用担心1%的性能差异。
阿特伍德在一个月前曾经发表过一篇关于这方面的文章:
是。 如果你做的不止两个连接,速度会快很多 。
当你做一个string.join时,运行时必须:
- 为结果string分配内存
- 将第一个string的内容复制到输出string的开头
- 将第二个string的内容复制到输出string的末尾。
如果你做了两次连接,它必须复制两次数据,依此类推。
StringBuilder分配一个空余空间的缓冲区,所以可以在不需要复制原始string的情况下添加数据。 由于缓冲区中剩余的空间,附加的string可以直接写入缓冲区。 然后它只需要复制整个string一次。
当然!
StringBuilder不是线程安全的,但推荐用于具有string操作的单线程程序。