提问者:小点点

StringBuilder.Append与StringBuilder.AppendFormat


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

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

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

共3个答案

匿名用户

不知道string1string2的大小,这是不可能的。

通过调用appendformater,只要给定格式字符串的长度和将要插入的字符串,它就会预先分配缓冲区,然后将所有内容串联起来并将其插入到缓冲区中。对于非常大的字符串,这将优于单独调用append,后者可能会导致缓冲区多次扩展。

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

需要更多的数据才能作出明确的答复

需要注意的是,很少讨论在String类上使用静态concat方法(Jon使用AppendWithCapital的回答提醒了我这一点)。他的测试结果表明,要成为最好的情况(假设您不必利用特定的格式说明符)。string.concat做了同样的事情,它将预先确定要连接的字符串的长度,并预先分配缓冲区(由于通过参数循环构造,开销略大)。它的性能将与Jon的AppendWithCapital方法相当。

或者,只使用普通加法运算符,因为它无论如何都会编译为对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很可能会在第二个测试期间介入,并阻碍其计时。

匿名用户

卡斯珀隆是对的。一旦达到某个阈值,append()方法就会变得比appendFormat()慢。下面是每种方法100,000次迭代的不同长度和经过的节拍:

Append()       - 50900
AppendFormat() - 126826
Append()       - 1241938
AppendFormat() - 1337396
Append()       - 12482051
AppendFormat() - 12740862
Append()       - 61029875
AppendFormat() - 60483914

当引入长度接近20,000的字符串时,appendFormat()函数的性能将略优于append()

为什么会出现这种情况?看卡斯佩龙的回答。

编辑:

我在Release configuration下单独重新运行每个测试,并更新结果。

匿名用户

casperOne完全准确地说,这取决于数据。但是,假设您将它作为第三方使用的类库来编写-您将使用哪一个呢?

一个选择是兼顾两个方面--计算出实际需要追加多少数据,然后使用StringBuilder.EnsureCapacity来确保我们只需要一次缓冲区调整大小。

不过,如果我不觉得太麻烦的话,我会使用appendX3-它似乎“更有可能”更快,因为在每个调用上解析字符串格式令牌显然是非常有用的。

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

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

不幸的是,表并不能在SO中工作。长度为1,1000,10000,20000

次数:

  • 追加:162,475,7997,17970
  • AppendFormat:392,499,8541,18993
  • AppendWithCapacity: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);
    }
}