String.Format和StringBuilder哪个更高效?

165

假设我在C#中有一个StringBuilder,它执行以下操作:

StringBuilder sb = new StringBuilder();
string cat = "cat";
sb.Append("the ").Append(cat).(" in the hat");
string s = sb.ToString();

那种方式是否与以下方式同样有效或更有效:

string cat = "cat";
string s = String.Format("The {0} in the hat", cat);
如果是这样,为什么?
编辑
看了一些有趣的答案后,我意识到我可能应该更清楚地表达我的问题。我不是在问哪个更快地连接字符串,而是在问哪个更快地将一个字符串注入到另一个字符串中。
在上面两种情况中,我都想将一个或多个字符串注入到预定义模板字符串的中间。
对于造成的困惑感到抱歉。

请将这些保持开放,以便未来进行改进。 - Mark Biek
4
在一个特殊的情况下,最快的方法并不是使用之前提到的两种方法中的任何一种:如果要替换的部分与新部分的大小相等,那么可以直接在原字符串中进行替换。不幸的是,这需要反射或不安全的代码,并且有意违反了字符串的不可变性。虽然这不是一个好的实践,但如果速度是一个问题...... :) - Abel
在上面的例子中, string s = "The "+cat+" in the hat"; 可能是最快的,除非它用于循环,在这种情况下,最快的方法是使用在循环之外初始化的 StringBuilder - Surya Pratap
12个回答

148

注意:此答案是在.NET 2.0版本为当前版本时编写的。这可能不再适用于较新的版本。

String.Format在内部使用StringBuilder

public static string Format(IFormatProvider provider, string format, params object[] args)
{
    if ((format == null) || (args == null))
    {
        throw new ArgumentNullException((format == null) ? "format" : "args");
    }

    StringBuilder builder = new StringBuilder(format.Length + (args.Length * 8));
    builder.AppendFormat(provider, format, args);
    return builder.ToString();
}

上面的代码片段来自mscorlib,所以问题变成了“StringBuilder.Append()StringBuilder.AppendFormat()快吗”?
没有进行基准测试,我可能会说使用.Append()运行的代码示例会更快。但这只是猜测,请尝试进行基准测试和/或分析这两个方法以获得适当的比较。
这位名叫Jerry Dixon的人进行了一些基准测试:

http://jdixon.dotnetdevelopersjournal.com/string_concatenation_stringbuilder_and_stringformat.htm

更新:

遗憾的是上面的链接已经失效了。不过在Way Back Machine上仍有一份备份:

http://web.archive.org/web/20090417100252/http://jdixon.dotnetdevelopersjournal.com/string_concatenation_stringbuilder_and_stringformat.htm

归根结底,这取决于你的字符串格式化是否会被重复调用,即是否在处理数百兆字节的文本时进行严格的文本处理,或者仅在用户偶尔点击按钮时进行调用。除非你正在执行一些巨大的批处理作业,否则建议使用String.Format,它有助于提高代码可读性。如果你怀疑性能瓶颈,请在你的代码上安装分析器并查看真正的问题所在。


9
Jerry Dixon的页面上基准测试存在一个问题,即他从未在StringBuilder对象上调用.ToString()。经过许多次迭代,这段时间会带来很大的差异,这意味着他没有完全拿到可比较的结果。这就是为什么他展示StringBuilder非常出色的性能的原因,也可能解释他感到惊讶的原因。我纠正了这个错误并重复了基准测试,得到了预期的结果:String +运算符最快,其次是StringBuilder,而String.Format最慢。 - Ben Collins
6
6年后,情况并非如此。在Net4中,string.Format()创建和缓存一个StringBuilder实例,并重复使用它,因此在某些测试用例中,它可能比StringBuilder更快。我在下面的答案中放置了一个修订后的基准测试(仍然显示concat最快,对于我的测试用例,format比StringBuilder慢10%)。 - Chris F Carroll

45

来自MSDN文档的内容:

String或StringBuilder对象的连接操作的性能取决于内存分配发生的频率。对于字符串连接操作,总是会分配内存,而对于StringBuilder连接操作,只有在StringBuilder对象缓冲区无法容纳新数据时才会分配内存。因此,如果要连接固定数量的字符串对象,则首选String类。在这种情况下,编译器甚至可能将各个连接操作合并为一个操作。如果要连接任意数量的字符串,例如在循环中连接随机数量的用户输入字符串,则首选StringBuilder对象。


12

我进行了一些快速性能基准测试,对于10次运行的100,000个操作的平均值来说,第一种方法(字符串生成器)需要的时间几乎是第二种方法(字符串格式化)的一半。

所以,如果这是不经常出现的情况,那就没关系。但如果这是一个常见操作,那么您可能希望使用第一种方法。


10

我认为 String.Format 的速度会比较慢 - 它需要对字符串进行解析,然后再拼接。

以下几点需要注意:

  • Format 是专业应用程序中用于用户可见字符串的首选方式;这样可以避免本地化错误。
  • 如果您事先知道结果字符串的长度,请使用 StringBuilder(Int32) 构造函数来预定义容量。

8
如果仅仅是因为string.Format并不完全符合你的想法,那么这里再次测试Net45关于字符串操作的性能,距离上次已有6年时间。
Concatenation仍然是最快的,但实际上区别不到30%。StringBuilder和Format仅相差5-10%。测试多次可以得到20%的变化。
毫秒数,一百万次迭代:
- Concatenation: 367 - 每个键创建新的stringBuilder:452 - 缓存的StringBuilder:419 - string.Format: 475
我的结论是性能差异微不足道,所以不应该阻止您编写尽可能简单易读的代码。对我来说,通常但并非总是最好的选择是使用类似"a + b + c"的方法。
const int iterations=1000000;
var keyprefix= this.GetType().FullName;
var maxkeylength=keyprefix + 1 + 1+ Math.Log10(iterations);
Console.WriteLine("KeyPrefix \"{0}\", Max Key Length {1}",keyprefix, maxkeylength);

var concatkeys= new string[iterations];
var stringbuilderkeys= new string[iterations];
var cachedsbkeys= new string[iterations];
var formatkeys= new string[iterations];

var stopwatch= new System.Diagnostics.Stopwatch();
Console.WriteLine("Concatenation:");
stopwatch.Start();

for(int i=0; i<iterations; i++){
    var key1= keyprefix+":" + i.ToString();
    concatkeys[i]=key1;
}

Console.WriteLine(stopwatch.ElapsedMilliseconds);

Console.WriteLine("New stringBuilder for each key:");
stopwatch.Restart();

for(int i=0; i<iterations; i++){
    var key2= new StringBuilder(keyprefix).Append(":").Append(i.ToString()).ToString();
    stringbuilderkeys[i]= key2;
}

Console.WriteLine(stopwatch.ElapsedMilliseconds);

Console.WriteLine("Cached StringBuilder:");
var cachedSB= new StringBuilder(maxkeylength);
stopwatch.Restart();

for(int i=0; i<iterations; i++){
    var key2b= cachedSB.Clear().Append(keyprefix).Append(":").Append(i.ToString()).ToString();
    cachedsbkeys[i]= key2b;
}

Console.WriteLine(stopwatch.ElapsedMilliseconds);

Console.WriteLine("string.Format");
stopwatch.Restart();

for(int i=0; i<iterations; i++){
    var key3= string.Format("{0}:{1}", keyprefix,i.ToString());
    formatkeys[i]= key3;
}

Console.WriteLine(stopwatch.ElapsedMilliseconds);

var referToTheComputedValuesSoCompilerCantOptimiseTheLoopsAway= concatkeys.Union(stringbuilderkeys).Union(cachedsbkeys).Union(formatkeys).LastOrDefault(x=>x[1]=='-');
Console.WriteLine(referToTheComputedValuesSoCompilerCantOptimiseTheLoopsAway);

2
通过“string.Format并不完全做你想象中的事情”我是指在4.5源代码中,它尝试创建和重用缓存的StringBuilder实例。因此,我在测试中包含了这种方法。 - Chris F Carroll

8

我认为在大多数情况下,清晰度而不是效率应该是你最关心的问题。除非你正在处理大量字符串或构建低功率移动设备的东西,否则这可能不会对运行速度产生太大影响。

我发现,在我构建字符串的情况下,要么直接连接要么使用StringBuilder是您的最佳选择。我建议在大多数构建的字符串中,其中大部分是动态的情况下使用。由于很少有文本是静态的,因此最重要的是清楚地知道每个动态文本放置的位置,以防将来需要更新。

另一方面,如果您正在谈论一个大块的静态文本,其中有两个或三个变量,即使它的效率稍微低一些,我认为从string.Format获得的清晰度是值得的。本周早些时候,当我必须将一个动态文本放置在4页文档的中心时,我使用了这种方法。如果大块文本是整体而不是三个连接在一起的部分,则更容易更新。


当你需要格式化字符串时,使用String.Format是明智的选择。当你需要执行机械拼接时,使用字符串连接或StringBuilder。始终努力选择能够清晰传达你意图给下一个维护者的方法。 - Rob

7

String.Format在内部使用StringBuilder,因此逻辑上会导致它具有更多开销,因此可能会稍微降低性能。然而,简单的字符串连接是将一个字符串插入到另外两个字符串中最快的方法,并且速度明显更快。这一事实由Rico Mariani在他的第一个性能测验中得出的证据,多年以前就已经展示过了。简单的事实是,当已知字符串部分的数量(无限制——可以连接千个部分,只要您知道它总是1000个部分)时,连接总是比StringBuilderString.Format更快。它们可以通过单个内存分配和一系列内存复制来执行。 这里是证明。

以下是一些String.Concat方法的实际代码,最终调用FillStringChecked,该方法使用指针来复制内存(通过Reflector提取):

public static string Concat(params string[] values)
{
    int totalLength = 0;

    if (values == null)
    {
        throw new ArgumentNullException("values");
    }

    string[] strArray = new string[values.Length];

    for (int i = 0; i < values.Length; i++)
    {
        string str = values[i];
        strArray[i] = (str == null) ? Empty : str;
        totalLength += strArray[i].Length;

        if (totalLength < 0)
        {
            throw new OutOfMemoryException();
        }
    }

    return ConcatArray(strArray, totalLength);
}

public static string Concat(string str0, string str1, string str2, string str3)
{
    if (((str0 == null) && (str1 == null)) && ((str2 == null) && (str3 == null)))
    {
        return Empty;
    }

    if (str0 == null)
    {
        str0 = Empty;
    }

    if (str1 == null)
    {
        str1 = Empty;
    }

    if (str2 == null)
    {
        str2 = Empty;
    }

    if (str3 == null)
    {
        str3 = Empty;
    }

    int length = ((str0.Length + str1.Length) + str2.Length) + str3.Length;
    string dest = FastAllocateString(length);
    FillStringChecked(dest, 0, str0);
    FillStringChecked(dest, str0.Length, str1);
    FillStringChecked(dest, str0.Length + str1.Length, str2);
    FillStringChecked(dest, (str0.Length + str1.Length) + str2.Length, str3);
    return dest;
}

private static string ConcatArray(string[] values, int totalLength)
{
    string dest = FastAllocateString(totalLength);
    int destPos = 0;

    for (int i = 0; i < values.Length; i++)
    {
        FillStringChecked(dest, destPos, values[i]);
        destPos += values[i].Length;
    }

    return dest;
}

private static unsafe void FillStringChecked(string dest, int destPos, string src)
{
    int length = src.Length;

    if (length > (dest.Length - destPos))
    {
        throw new IndexOutOfRangeException();
    }

    fixed (char* chRef = &dest.m_firstChar)
    {
        fixed (char* chRef2 = &src.m_firstChar)
        {
            wstrcpy(chRef + destPos, chRef2, length);
        }
    }
}

因此,接下来:
string what = "cat";
string inthehat = "The " + what + " in the hat!";

享受!


在Net4中,string.Format缓存并重用StringBuilder实例,因此在某些用法中可能更快。 - Chris F Carroll

3

另外,最快的方法是:

string cat = "cat";
string s = "The " + cat + " in the hat";

不,字符串拼接非常慢,因为.NET在连接操作之间创建了字符串变量的额外副本,在这种情况下:两个额外副本加上最终赋值的副本。结果:与专门优化此类编码的StringBuilder相比,性能极差。 - Abel
2
@Abel:答案可能缺少细节,但在这个特定的例子中,这种方法确实是最快的选择。编译器将把它转换为单个String.Concat()调用,因此用StringBuilder替换实际上会减慢代码速度。 - Dan C.
1
@Vaibhav 是正确的:在这种情况下,连接是最快的。当然,除非重复很多次,或者操作的字符串非常大,否则差异微不足道。 - Ben Collins

0

0

这真的取决于情况。对于少量连接的小字符串,直接附加字符串实际上更快。

String s = "String A" + "String B";

但是对于更大的字符串(非常非常大的字符串),使用StringBuilder更有效率。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接