代码之家  ›  专栏  ›  技术社区  ›  Hosam Aly

字符串。Join与StringBuilder:哪个更快?

  •  98
  • Hosam Aly  · 技术社区  · 17 年前

    在一个 previous question double[][] it was suggested 使用 StringBuilder 会比 String.Join

    5 回复  |  直到 6 年前
        1
  •  88
  •   Jon Skeet    9 年前

    简短的回答:这取决于。

    如果你已经有一个字符串数组要连接在一起(用分隔符), String.Join 这是最快的方法。

    字符串。加入 涉及额外的复制。这 不利的一面是,它必须遍历字符串两次,这意味着可能会在不必要的情况下多次清空内存缓存。

    如果你 不要 预先将字符串作为数组,它是 可能 使用更快 StringBuilder 字符串构建器 字符串。加入 可能会更快。

    字符串。加入 StringBuilder.Append 在最初的问题中,我们有两个不同的层次 字符串。加入 调用,因此每个嵌套调用都会创建一个中间字符串。换句话说,它甚至更复杂,更难猜测。我会惊讶地看到,无论哪种方式,典型数据都能显著“获胜”(从复杂性的角度来看)。

    编辑:当我在家的时候,我会写一个尽可能痛苦的基准测试 字符串构建器 字符串。加入 不会有任何问题。

        2
  •  29
  •   Marc Gravell    17 年前

    这是我的测试装置,使用 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();
            }
        }
    }
    
        3
  •  17
  •   MSeifert    8 年前

    String.Join 看起来非常优化。它还有一个额外的好处,即提前知道要创建的字符串的总大小,因此不需要任何重新分配。

    我创建了两种测试方法来比较它们:

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

    [2048][64] 我为两个数组做了这件事;一个填充零,另一个填充随机值。我在我的机器上得到了以下结果(P4 3.0 GHz,单核,无HT,从CMD运行Release模式):

    // 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] ,虽然将迭代次数减少到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
    

    结果是可重复的(几乎;由不同的随机值引起的微小波动)。显然 字符串。加入 大多数时候都有点快(尽管幅度很小)。

    这是我用于测试的代码:

    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));
        }
    }
    
        4
  •  8
  •   tvanfosson    17 年前

    除非1%的差异在整个程序运行所需的时间方面变得显著,否则这看起来像是微观优化。我会编写最可读/最容易理解的代码,而不用担心1%的性能差异。

        5
  •  -1
  •   Adam Neal    17 年前

    对。如果你做了几次以上的连接,它将是 很多 更快。

    当你执行string.join时,运行时必须:

    1. 将第一个字符串的内容复制到输出字符串的开头
    2. 将第二个字符串的内容复制到输出字符串的末尾。

    如果进行两次连接,则必须复制数据两次,依此类推。

    StringBuilder会分配一个缓冲区作为备用空间,这样就可以在不复制原始字符串的情况下追加数据。由于缓冲区中还有剩余空间,追加的字符串可以直接写入缓冲区。 然后,它只需在末尾复制整个字符串一次。

        6
  •  -3
  •   robasta    13 年前
    推荐文章