【问题标题】:Why is StringBuilder slower than string concatenation?为什么 StringBuilder 比字符串连接慢?
【发布时间】:2011-12-26 15:56:07
【问题描述】:

为什么StringBuilder 比 + 连接慢? StringBuilder 是为了避免创建额外的对象,但为什么会影响性能?

    static void Main(string[] args)
    {
        int max = 1000000;
        for (int times = 0; times < 5; times++)
        {
            Console.WriteLine("\ntime: {0}", (times+1).ToString());
            Stopwatch sw = Stopwatch.StartNew();
            for (int i = 0; i < max; i++)
            {
                string msg = "Your total is ";
                msg += "$500 ";
                msg += DateTime.Now;
            }
            sw.Stop();
            Console.WriteLine("String +\t: {0}ms", ((int)sw.ElapsedMilliseconds).ToString().PadLeft(6));

            sw = Stopwatch.StartNew();
            for (int j = 0; j < max; j++)
            {
                StringBuilder msg = new StringBuilder();
                msg.Append("Your total is ");
                msg.Append("$500 ");
                msg.Append(DateTime.Now);
            }
            sw.Stop();
            Console.WriteLine("StringBuilder\t: {0}ms", ((int)sw.ElapsedMilliseconds).ToString().PadLeft(6));
        }
        Console.Read();
    }

编辑:按照建议移出范围变量:

【问题讨论】:

  • 我认为 StringBuilder 对于“更大”的字符串更快。
  • 添加string.Format和string.Concat,我们也注意到string.Concat更快了
  • 它几乎总是更快,除非你正在做一个简单的单一连接并且不值得额外的代码行。正如 NullUser 指出的那样,您在循环内部进行分配是错误的,并且会扭曲您的数字。
  • 为什么每个人都会自动假设字符串连接是“慢”的?重复的字符串连接有一个“更糟糕的 Big-O”,但不要忘记 C 并记住 n :) 使用正确的工具来完成这项工作——这是我拉出 StringBuilder 的罕见日子. (另外,我不知道 C# 做了哪些优化,但 Java 可能在编译期间将 + 转换为等效的 StringBuilder 代码。)
  • StringBuilder 旨在避免创建额外的对象 - 除非您出于某种未知原因在每次迭代中创建一个新的 StringBuilder...

标签: c# stringbuilder string-concatenation


【解决方案1】:

更改以便 StringBuilder 不会一直实例化,而是 .Clear() it:

time: 1
String +    :   3348ms
StringBuilder   :   3151ms

time: 2
String +    :   3346ms
StringBuilder   :   3050ms

等等。

注意,这仍然测试完全相同的功能,但尝试更智能地重用资源。

代码:(也在 http://ideone.com/YuaqY 上直播)

using System;
using System.Text;
using System.Diagnostics;

public class Program
{
    static void Main(string[] args)
    {
        int max = 1000000;
        for (int times = 0; times < 5; times++)
        {
            {
                Console.WriteLine("\ntime: {0}", (times+1).ToString());
                Stopwatch sw = Stopwatch.StartNew();
                for (int i = 0; i < max; i++)
                {
                    string msg = "Your total is ";
                    msg += "$500 ";
                    msg += DateTime.Now;
                }
                sw.Stop();
                Console.WriteLine("String +\t: {0}ms", ((int)sw.ElapsedMilliseconds).ToString().PadLeft(6));
            }

            {
                Stopwatch sw = Stopwatch.StartNew();
                StringBuilder msg = new StringBuilder();
                for (int j = 0; j < max; j++)
                {
                    msg.Clear();
                    msg.Append("Your total is ");
                    msg.Append("$500 ");
                    msg.Append(DateTime.Now);
                }
                sw.Stop();
                Console.WriteLine("StringBuilder\t: {0}ms", ((int)sw.ElapsedMilliseconds).ToString().PadLeft(6));
            }
        }
        Console.Read();
    }
}

【讨论】:

  • 确实我错过了.Clear()。 StringBuilder 的成本下降了很多。现在是3196ms!有时它会变慢,但考虑到这种调整,它已经有所改善。 :D
  • @JuniorMayhé 几百毫秒(超过 3000 毫秒)的差异并不显着。在处理大字符串时,使用 SB 和 + 会出现数量级的差异:ideone.com/SNBr1
  • @NullUserExceptionఠ_ఠ:我们都知道。我很好奇是否可以在不更改测试的情况下使其更快。事实证明,是的,你可以。当然,这是综合测试的一个坏例子,但它巧妙地驳斥了“StringBuilder”会更慢的观点:它不一定,即使用于非最佳任务
  • @sehe 确实,这也让我有点吃惊。 +1 展示它;)
  • 你的 ideone 链接给了我一个编译错误。 PS:喜欢你刚刚创建一个新范围而不是重命名变量的方式...... LOL
【解决方案2】:

每次迭代都会创建一个 StringBuilder 的新实例,这会产生一些开销。由于您没有将它用于它的实际用途(即:构建大字符串,否则需要许多字符串连接操作),因此看到比连接更差的性能也就不足为奇了。

StringBuilder 更常见的比较/用法是:

string msg = "";
for (int i = 0; i < max; i++)
{
    msg += "Your total is ";
    msg += "$500 ";
    msg += DateTime.Now;
}

StringBuilder msg_sb = new StringBuilder();
for (int j = 0; j < max; j++)
{
    msg_sb.Append("Your total is ");
    msg_sb.Append("$500 ");
    msg_sb.Append(DateTime.Now);
}

这样,您将观察到StringBuilder 和串联之间的显着性能差异。 “显着”是指 orders of magnitude,而不是您在示例中观察到的 ~ 10% 差异。

由于StringBuilder 不必构建大量会被丢弃的中间字符串,因此您可以获得更好的性能。这就是它的意义所在。对于较小的字符串,为了简单明了,最好使用字符串连接。

【讨论】:

  • 不过,你测量的东西也完全不同
  • +1 - 正如 NullUser 指出的,您只需要分配一次,然后追加到循环中
  • @JuniorMayhé 这是正确答案。像在您的示例中那样将 StringBuilder 和串联放在同一个循环中并不能说明全部情况,因为 StringBuilder 必须等待串联发生。您的字符串越大,StringBuilder 也变得越快。
  • @sehe 我知道;我相应地修改了我的答案。
【解决方案3】:

StringBuilder 的好处对于更长的字符串应该是显而易见的。

每次连接字符串时都会创建一个新的字符串对象,因此字符串越长,从旧字符串复制到新字符串所需的时间就越多。

此外,创建许多临时对象可能会对 StopWatch 无法衡量的性能产生不利影响,因为它会用临时对象“污染”托管堆,并可能导致更多的垃圾回收周期。

修改您的测试以创建(更多)更长的字符串并使用(许多)更多的连接/追加操作,并且 StringBuilder 应该表现更好。

【讨论】:

    【解决方案4】:

    注意

    string msg = "Your total is ";
    msg += "$500 ";
    msg += DateTime.Now;
    

    编译成

    string msg = String.Concat("Your total is ", "$500 ");
    msg = String.Concat(msg, DateTime.Now.ToString());
    

    每次迭代总共有两个 concats 和一个 ToString。另外,单个 String.Concat 确实很快,因为它知道生成的字符串会有多大,所以它只分配一次生成的字符串,然后快速将源字符串复制到其中。这意味着在实践中

    String.Concat(x, y);
    

    将永远优于

    StringBuilder builder = new StringBuilder();
    builder.Append(x);
    builder.Append(y);
    

    因为 StringBuilder 不能采用这样的快捷方式(您可以调用 thirs Append 或 Remove,这在 String.Concat 中是不可能的)。

    StringBuilder 的工作方式是分配一个初始缓冲区并将字符串长度设置为 0。对于每个 Append,它必须检查缓冲区,可能分配更多缓冲区空间(通常将旧缓冲区复制到新缓冲区),复制字符串并增加构建器的字符串长度。 String.Concat 不需要做所有这些额外的工作。

    因此,对于简单的字符串连接,x + y(即 String.Concat)将始终优于 StringBuilder。

    现在,一旦您开始将大量字符串连接到单个缓冲区中,或者您在缓冲区上进行大量操作,您将开始从 StringBuilder 中受益,如果没有,您需要继续创建新字符串使用 StringBuilder。这是因为 StringBuilder 只是偶尔以块的形式分配新内存,但 String.Concat、String.SubString 等(几乎)总是分配新内存。 (像 "".SubString(0,0) 或 String.Concat("", "") 这样的东西不会分配内存,但这些都是退化的情况。)

    【讨论】:

      【解决方案5】:

      除了没有以最有效的方式使用StringBuilder 之外,您也没有尽可能有效地使用字符串连接。如果您提前知道要连接多少个字符串,那么在一行上完成所有操作应该是最快的。编译器对操作进行了优化,因此不会生成中间字符串。

      我添加了几个测试用例。一个和sehe建议的基本一样,另一个在一行中生成字符串:

      sw = Stopwatch.StartNew();
      builder = new StringBuilder();
      for (int j = 0; j < max; j++)
      {
          builder.Clear();
          builder.Append("Your total is ");
          builder.Append("$500 ");
          builder.Append(DateTime.Now);
      }
      sw.Stop();
      Console.WriteLine("StringBuilder (clearing)\t: {0}ms", ((int)sw.ElapsedMilliseconds).ToString().PadLeft(6));
      
      sw = Stopwatch.StartNew();
      for (int i = 0; i < max; i++)
      {
          msg = "Your total is " + "$500" + DateTime.Now;
      }
      sw.Stop();
      Console.WriteLine("String + (one line)\t: {0}ms", ((int)sw.ElapsedMilliseconds).ToString().PadLeft(6));
      

      这是我在机器上看到的输出示例:

      time: 1
      String +    :   3707ms
      StringBuilder   :   3910ms
      StringBuilder (clearing)    :   3683ms
      String + (one line) :   3645ms
      
      time: 2
      String +    :   3703ms
      StringBuilder   :   3926ms
      StringBuilder (clearing)    :   3666ms
      String + (one line) :   3625ms
      

      一般来说: - StringBuilder 如果您要通过很多步骤构建一个大字符串,或者您不知道将有多少个字符串连接在一起,则效果会更好。
      - 只要是合理的选项,将它们全部混合在一个表达式中会更好。

      【讨论】:

        【解决方案6】:

        我认为最好比较 String 和 StringBuilder 之间的效率而不是时间。

        msdn 说: String 被称为不可变的,因为它的值一旦被创建就不能被修改。看似修改字符串的方法实际上返回一个包含修改的新字符串。如果需要修改类字符串对象的实际内容,请使用 System.Text.StringBuilder 类。

        string msg = "Your total is "; // a new string object
        msg += "$500 "; // a new string object
        msg += DateTime.Now; // a new string object
        

        看看哪个更好。

        【讨论】:

          【解决方案7】:

          下面的示例演示了StringBuilder 将比字符串连接更快执行的情况:

          static void Main(string[] args)
          {
              const int sLen = 30, Loops = 10000;
              DateTime sTime, eTime;
              int i;
              string sSource = new String('X', sLen);
              string sDest = "";
              // 
              // Time StringBuilder.
              // 
              for (int times = 0; times < 5; times++)
              {
                  sTime = DateTime.Now;
                  System.Text.StringBuilder sb = new System.Text.StringBuilder((int)(sLen * Loops * 1.1));
                  Console.WriteLine("Result # " + (times + 1).ToString());
                  for (i = 0; i < Loops; i++)
                  {
                      sb.Append(sSource);
                  }
                  sDest = sb.ToString();
                  eTime = DateTime.Now;
                  Console.WriteLine("String Builder took :" + (eTime - sTime).TotalSeconds + " seconds.");
                  // 
                  // Time string concatenation.
                  // 
                  sTime = DateTime.Now;
                  for (i = 0; i < Loops; i++)
                  {
                      sDest += sSource;
                      //Console.WriteLine(i);
                  }
                  eTime = DateTime.Now;
                  Console.WriteLine("Concatenation took : " + (eTime - sTime).TotalSeconds + " seconds.");
                  Console.WriteLine("\n");
              }
              // 
              // Make the console window stay open
              // so that you can see the results when running from the IDE.
              // 
          }
          

          结果 #1 字符串生成器耗时:0 秒。 连接耗时:8.7659616 秒。

          结果 #2 字符串生成器耗时:0 秒。 连接耗时:8.7659616 秒。

          结果 #3 字符串生成器耗时:0 秒。 连接耗时:8.9378432 秒。

          结果 #4 字符串生成器耗时:0 秒。 连接耗时:8.7972128 秒。

          结果 #5 字符串生成器耗时:0 秒。 连接耗时:8.8753408 秒。

          StringBulder 比 + 连接快得多..

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2012-10-16
            • 2011-11-10
            • 2020-12-18
            • 2016-10-13
            • 1970-01-01
            • 2023-03-08
            • 2011-07-22
            • 1970-01-01
            相关资源
            最近更新 更多