39

我想知道 StringBuilder 并且我有一个问题希望社区能够解释。

让我们忘记代码可读性,其中哪个更快,为什么?

StringBuilder.Append

StringBuilder sb = new StringBuilder();
sb.Append(string1);
sb.Append("----");
sb.Append(string2);

StringBuilder.AppendFormat

StringBuilder sb = new StringBuilder();
sb.AppendFormat("{0}----{1}",string1,string2);
4

9 回答 9

45

不可能说,不知道 和 的string1大小string2

通过调用AppendFormat,它会在给定格式字符串的长度和将要插入的字符串的情况下预分配一次缓冲区,然后将所有内容连接起来并将其插入缓冲区。对于非常大的字符串,这将优于Append可能导致缓冲区多次扩展的单独调用。

但是,三个调用Append可能会或可能不会触发缓冲区的增长,并且每次调用都会执行该检查。如果字符串足够小并且没有触发缓冲区扩展,那么它将比调用更快,AppendFormat因为它不必解析格式字符串来确定在哪里进行替换。

需要更多数据才能确定答案

应该注意的是,几乎没有讨论在上使用静态Concat方法String乔恩的回答AppendWithCapacity让我想起了这一点)。他的测试结果表明这是最好的情况(假设您不必利用特定的格式说明符)。 String.Concat做同样的事情,它会预先确定要连接和预分配缓冲区的字符串的长度(由于通过参数循环构造,开销会稍多一些)。它的性能将与 Jon 的AppendWithCapacity方法相媲美。

或者,只是普通的加法运算符,因为它编译为String.Concat无论如何调用,但需要注意的是所有加法都在同一个表达式中:

// One call to String.Concat.
string result = a + b + c;

不是

// Two calls to String.Concat.
string result = a + b;
result = result + c;

对于所有提交测试代码的人

您需要在单独的运行中运行您的测试用例(或者至少,在单独的测试运行的测量之间执行一次 GC)。这样做的原因是,如果你确实说,1,000,000 次运行,StringBuilder在循环的每次迭代中为一个测试创建一个新的,然后你运行下一个循环相同次数的测试,创建额外的1,000,000个StringBuilder实例,GC很可能会在第二次测试期间介入并阻碍其时间安排。

于 2009-04-02T16:32:58.730 回答
24

casperOne 是正确的。一旦达到某个阈值,该Append()方法就会变得比AppendFormat(). 以下是每种方法 100,000 次迭代的不同长度和经过的刻度:

长度:1

Append()       - 50900
AppendFormat() - 126826

长度:1000

Append()       - 1241938
AppendFormat() - 1337396

长度:10,000

Append()       - 12482051
AppendFormat() - 12740862

长度:20,000

Append()       - 61029875
AppendFormat() - 60483914

当引入长度接近 20,000 的字符串时,该函数AppendFormat()略胜一筹Append()

为什么会这样?请参阅casperOne 的回答

编辑:

我在发布配置下单独重新运行每个测试并更新了结果。

于 2009-04-02T16:46:05.253 回答
13

casperOne 完全准确,这取决于数据。但是,假设您将其作为类库编写以供第 3 方使用 - 您会使用哪个?

一种选择是两全其美 - 计算出您实际上需要附加多少数据,然后使用StringBuilder.EnsureCapacity确保我们只需要调整单个缓冲区大小。

如果我不太介意的话,我会使用Appendx3 - 似乎“更有可能”更快,因为在每次调用时解析字符串格式标记显然是制作工作。

请注意,我已经向 BCL 团队询问了一种“缓存格式化程序”,我们可以使用格式字符串创建它,然后重复使用。框架每次使用时都必须解析格式字符串,这太疯狂了。

编辑:好的,为了灵活性,我对 John 的代码进行了一些编辑,并添加了一个“AppendWithCapacity”,它首先计算出必要的容量。以下是不同长度的结果 - 对于长度 1,我使用了 1,000,000 次迭代;对于所有其他长度,我使用了 100,000。(这只是为了获得合理的运行时间。)所有时间都以毫秒为单位。

不幸的是,表格在 SO 中并没有真正起作用。长度分别为 1、1000、10000、20000

时间:

  • 追加:162、475、7997、17970
  • 附加格式:392、499、8541、18993
  • 附加容量:139、189、1558、3085

事情发生了,我从未见过 AppendFormat 击败 Append - 但我确实看到 AppendWithCapacity 以非常可观的优势获胜。

这是完整的代码:

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

public class StringBuilderTest
{            
    static void Append(string string1, string string2)
    {
        StringBuilder sb = new StringBuilder();
        sb.Append(string1);
        sb.Append("----");
        sb.Append(string2);
    }

    static void AppendWithCapacity(string string1, string string2)
    {
        int capacity = string1.Length + string2.Length + 4;
        StringBuilder sb = new StringBuilder(capacity);
        sb.Append(string1);
        sb.Append("----");
        sb.Append(string2);
    }

    static void AppendFormat(string string1, string string2)
    {
        StringBuilder sb = new StringBuilder();
        sb.AppendFormat("{0}----{1}", string1, string2);
    }

    static void Main(string[] args)
    {
        int size = int.Parse(args[0]);
        int iterations = int.Parse(args[1]);
        string method = args[2];

        Action<string,string> action;
        switch (method)
        {
            case "Append": action = Append; break;
            case "AppendWithCapacity": action = AppendWithCapacity; break;
            case "AppendFormat": action = AppendFormat; break;
            default: throw new ArgumentException();
        }

        string string1 = new string('x', size);
        string string2 = new string('y', size);

        // Make sure it's JITted
        action(string1, string2);
        GC.Collect();

        Stopwatch sw = Stopwatch.StartNew();
        for (int i=0; i < iterations; i++)
        {
            action(string1, string2);
        }
        sw.Stop();
        Console.WriteLine("Time: {0}ms", (int) sw.ElapsedMilliseconds);
    }
}
于 2009-04-02T17:05:19.217 回答
6

Append在大多数情况下会更快,因为该方法有许多重载,允许编译器调用正确的方法。由于您使用Strings的是StringBuilder可以将String重载用于Append.

AppendFormat接受 aString然后是 an Object[],这意味着必须解析格式,并且Object数组中的每个都必须ToString'd在它可以添加到StringBuilder's内部数组之前。

注意:就 casperOne 而言 - 如果没有更多数据,很难给出准确的答案。

于 2009-04-02T16:32:40.930 回答
2

StringBuilder也有级联附加:Append()返回StringBuilder本身,所以你可以这样写你的代码:

StringBuilder sb = new StringBuilder();
sb.Append(string1)
  .Append("----")
  .Append(string2);

干净,并且它生成的 IL 代码更少(尽管这实际上是一个微优化)。

于 2009-04-02T17:14:59.770 回答
1

当然轮廓在每种情况下都可以肯定地知道。

也就是说,我认为通常会是前者,因为您不会重复解析格式字符串。

但是,差异会非常小。无论如何,您确实应该考虑AppendFormat在大多数情况下使用。

于 2009-04-02T16:31:15.193 回答
0

我认为这是做最少工作量的电话。Append 只是连接字符串,其中 AppendFormat 正在执行字符串替换。当然,这些天,你永远无法告诉......

于 2009-04-02T16:31:30.200 回答
0

1 应该更快,因为它只是附加字符串,而 2 必须根据格式创建一个字符串,然后附加字符串。所以那里有一个额外的步骤。

于 2009-04-02T16:31:50.963 回答
0

在您的情况下,更快是 1,但这不是一个公平的比较。您应该问StringBuilder.AppendFormat()vs StringBuilder.Append(string.Format())- 由于内部使用 char 数组,第一个更快。

不过,您的第二个选项更具可读性。

于 2009-04-02T16:35:34.293 回答