在什么情况下使用StringBuilder会变得无关紧要或者成为负担?

18

最近我发现自己在进行所有字符串连接时都使用了StringBuilder,无论是大还是小。但是最近一次性能测试中,我替换了同事使用的stringOut = string1 + "." string2样式的连接方式(在一个10000x +循环中使用,每次都新建StringBuilder),只是为了看看在小型连接中会有什么区别。

经过多次运行性能测试,我发现无论是对于连接还是StringBuilder,变化都微不足道(重申这是针对小型连接)。

在什么时候,创建新的StringBuilder对象会抵消使用它的好处?


@lagerdalek,你能发一下这段代码的例子吗?“每次都新建StringBuilder”听起来有点可疑。 - LukeH
看起来是这样的: for (int i = 0; i < someBigNum; ++0 { Stringbuilder sb = new StringBuilder(); sb.Append(var1); sb.Append(var2); sb.Append(var3); }StringBuilder 不是一个好的选择。 - Ed S.
@Ed,我也是这么怀疑的。 - LukeH
@Luke和Ed,非常正确,正如我所说,这只是一个简单的测试。如果两种方法之间存在显着差异,我认为它会在大循环中被放大。 "不适合使用StringBuilder"是我怀疑、测试并显然证实的。感谢大家。 - johnc
7个回答

20

我遵循的规则是 -

当连接字符串的数量在编译时不确定时,使用 StringBuilder。

所以,在您的情况下,每个 StringBuilder 只附加了几次然后被丢弃。这与类似以下情况并不完全相同:

string s = String.Empty;
for (int i = 0; i < 10000; ++i)
{
    s += "A";
}

如果你不使用StringBuilder,就会不断地分配新内存,而使用它可以显著提高性能。


18

我确定我曾经在另一个答案中发布过一篇链接到我文章的链接,然后是它的摘要,但我们再来一次。

  • 当您在非平凡循环中进行连接操作时,请务必使用StringBuilder - 特别是如果您不确定(在编译时)将通过循环进行多少次迭代。例如,每次读取文件一个字符,使用+=运算符逐步构建字符串可能会导致性能下降。

  • 当您可以(易于阅读地)指定需要连接的所有内容时,请务必使用连接运算符。(如果您有一个要连接的事物数组,请考虑显式调用String.Concat - 或者如果您需要分隔符,则调用String.Join。)

  • 放心地将字面值拆分成几个连接的片段 - 结果将是相同的。例如,您可以通过将长字面量分成几行来提高可读性,而不会影响性能。

  • 如果您需要连接的中间结果不仅用于馈送连接的下一次迭代,则StringBuilder对您没有帮助。例如,如果您从名字和姓氏构建完整名称,然后将第三个信息(例如昵称)添加到末尾,则只有在您不需要(名字+姓氏)字符串以供其他目的时才会从使用StringBuilder中受益(就像我们在创建Person对象的示例中所做的那样)。

  • 如果你只需要连接几个东西,并且你真的想在单独的语句中连接它们,那么无论选择哪种方式都没关系。哪种方式更高效取决于连接数、涉及字符串的大小以及它们连接的顺序。如果您真的认为该代码是性能瓶颈,请对其进行剖析或基准测试。


2
在不仅仅发布链接的情况下进行总结(孤立的链接复述是我的一个讨厌之一)+1。 - johnc
如果你需要中间结果,有一个小细节需要补充,但是如果你确实想使用 StringBuilder,可以使用 .ToString(0, .Length) 来确保你得到一个全新的副本,并且不会影响正在构建的字符串的性能。 - Mark Hurd

6
有时候看一下文档是值得的:

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

在您的示例中,每个输出字符串只有一个连接操作,因此使用StringBuilder没有任何优势。您应该在添加到同一字符串多次的情况下使用StringBuilder,例如:

stringOut = ...
for(...)
    stringOut += "."
    stringOut += string2

2

我的经验法则很简单。

  1. 如果你可以合理地编写一个产生最终结果的单个表达式,那么使用 +
  2. 如果不能(由于大小或可变性),则使用 StringBuilder。

根据我的经验,以下表达式:

"Id: " + item.id + " name: " + item.name

可以更加简单易懂地书写和理解,而不是:

StringBuilder sb = new StringBuilder();
sb.append("Id: ").append(item.id);
sb.append(" name: ").append(item.name);

(followed by using the string from sb where the above expression would have been written), and it performs equally well (hint: look at the compiled code to see why!)
另一方面,当需要随着程序运行而逐渐累积一个字符串或空间(由来自代码不同部分的值组成),以一种单行表达式无法实现的方式时,使用StringBuilder可以避免以下开销(时间和内存磨损):
String s = somethingExpression;
...
s += someOtherExpression;
...
s += yetAnotherExpression;
...

1

来自MSDN

如果要连接固定数量的字符串,则使用String类更好。在这种情况下,编译器甚至可以将单个连接操作合并为单个操作。如果要连接任意数量的字符串,例如循环连接用户输入的随机数量的字符串,则StringBuilder对象更好。

我猜答案是“取决于情况”-如果您在超过少数迭代的循环中进行连接,则StringBuilder几乎总是会提供更好的性能,但确定最好的方法是实际进行性能分析。


1

来自Dot Net Perls

何时使用StringBuilder?

StringBuilder完全是一种优化,除了其内部实现之外,它对于字符串Concat没有逻辑上的改进。也就是说,在高性能应用程序和网站中正确地使用它非常重要。

有时,使用简单的字符串连续运算进行4个或更少迭代的小循环是可以的。然而,在极端情况下,这可能会带来灾难性后果。使用StringBuilder来规划您的边缘情况。


1

Coding Horror上有一篇有趣的文章。Jeff在双核3.5 GHz Core 2 Duo上进行了100,000次迭代,得到了以下结果:

 Simple Concatenation    - 606 ms
 String.Format           - 665 ms
 string.Concat           - 587 ms
 String.Replace          - 979 ms
 StringBuilder           - 588 ms

是的,但那是每次连接固定数量的东西。我同意 Ed 的观点--如果要连接的东西的数量本质上是可变的,你需要使用 StringBuilder。 - Jeff Atwood
1
好的,规则是smushed == StringBuilder。谢谢 :) - johnc

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