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%的性能差异。

阿特伍德在一个月前曾经发表过一篇关于这方面的文章:

http://www.codinghorror.com/blog/archives/001218.html

是。 如果你做的不止两个连接,速度会快很多

当你做一个string.join时,运行时必须:

  1. 为结果string分配内存
  2. 将第一个string的内容复制到输出string的开头
  3. 将第二个string的内容复制到输出string的末尾。

如果你做了两次连接,它必须复制两次数据,依此类推。

StringBuilder分配一个空余空间的缓冲区,所以可以在不需要复制原始string的情况下添加数据。 由于缓冲区中剩余的空间,附加的string可以直接写入缓冲区。 然后它只需要复制整个string一次。

当然!

StringBuilder不是线程安全的,但推荐用于具有string操作的单线程程序。