为什么String.Concat没有针对StringBuilder.Append进行优化?

时间:2010-02-01 14:47:12

标签: c# .net string optimization clr

我发现常量字符串表达式的连接由编译器优化为一个字符串。

现在,只有在运行时才知道字符串的字符串连接,为什么编译器不会优化循环中的字符串连接以及多于10个字符串的连接而使用StringBuilder.Append?我的意思是,这是可能的,对吧?实例化StringBuilder并将每个连接转换为Append()来电。

我错过了什么?

8 个答案:

答案 0 :(得分:41)

明确的答案必须来自编译器设计团队。但是让我在这里捅一下......

如果你的问题是,编译器为什么不转这个:

string s = "";
for( int i = 0; i < 100; i ++ )
    s = string.Concat( s, i.ToString() );

进入这个:

StringBuilder sb = new StringBuilder();
for( int i = 0; i < 100; i++ )
    sb.Append( i.ToString() );
string s = sb.ToString();

最可能的答案是这不是优化。这是对代码的重写,它引入了基于开发人员所具有的知识和意图的新构造 - 而不是编译器。

这种类型的更改需要编译器对BCL有更多的了解。如果明天会有更优化的字符串组装服务怎么办?编译器应该使用它吗?

如果您的循环条件更复杂,如果编译器尝试执行某些静态分析以确定此类重写的结果是否仍然在功能上等效,那该怎么办?在许多方面,这就像解决halting problem

最后,我不确定在所有情况下这都会导致代码执行速度更快。实例化StringBuilder并在附加文本时调整其内部缓冲区的大小需要付出代价。实际上,追加的成本与被连接的字符串的大小,有多少,内存压力看起来很紧密相关。这些是编译器无法预先预测的事情。

作为开发人员,您的工作就是编写性能良好的代码。编译器只能通过进行某些安全,保持不变的优化来提供帮助。不要为你重写代码。

答案 1 :(得分:30)

LBuskin的答案非常好;我只想补充几件事。

首先,JScript.NET会进行此优化。 JScript经常被经验较少的程序员用于涉及在循环中构造大字符串的任务,例如构建JSON对象,HTML数据等。

由于那些程序员可能不知道天真字符串分配的n平方成本,可能不知道字符串构建器的存在,并且经常使用这种模式编写代码,我们觉得这样做是合理的。进入JScript.NET。

C#程序员倾向于更多地意识到他们编写的代码的底层成本,并且更加了解StringBuilder等现成部件的存在,因此他们需要更少的优化。而更重要的是,C#的设计理念是,它是一种“尽我所能”的语言,至少具有“魔力”; JScript是一种“尽我所能”的语言,它尽力弄清楚如何最好地为您服务,即使这意味着有时候猜错了。这两种哲学都是有效的。

有时它会“走另一条路”。将此选项与我们对字符串开关的选择进行比较。字符串上的开关实际上被编译为包含字符串的字典的创建,而不是作为一系列字符串比较。这种优化可能很糟糕;简单地进行字符串比较可能会更快。但是在这里我们猜测你“意味着”转换为表查找而不是一系列“if”语句 - 如果你的意思是if语句系列,你可以自己轻松地写出来。

答案 2 :(得分:16)

对于多个字符串的单个串联(例如a + b + c + d + e + f + g + h + i + j),您确实希望使用String.Concat IMO。它有为每个调用构建一个数组的开销,但它的好处是该方法可以在需要分配任何内存之前计算出结果字符串的确切长度。 StringBuilder.Append(a).Append(b)...一次只提供一个值,因此构建器不知道要分配多少内存。

至于在循环中执行 - 此时您已添加了一个新的局部变量,并且您必须添加代码以在恰当的时间写回字符串变量(调用StringBuilder.ToString()) 。当你在调试器中运行时会发生什么?如果不看到价值积累,只是在循环结束时变得可见,难道不会让人感到困惑吗?哦,当然你必须进行适当的验证,在循环结束之前的任何时候都不使用该值......

答案 3 :(得分:6)

有两个原因:

  • 您无法以编程方式识别出性能更高的地方。
  • 如果执行不正确,“优化”会降低速度。

您可以建议人们对他们的应用程序使用正确的调用,但在某些时候,开发人员有责任正确使用它。

编辑:关于截止点,我们还有另外两个问题:

  • 了解肯定达到截止值的唯一方法是复杂的流量分析。能够找到可以转换的部分的地方数量非常少。
  • 流量分析很昂贵。如果你在运行时这样做,整个程序将运行得更慢,因为很少有机会编写一段写得不好的代码。如果你在编译时这样做,根据语言语法不是错误,但你可以发出警告 - 这正是FXCop所做的(一个缓慢但可用的流分析工具)。试想一下FXCop是否总是必须与编译器一起运行;这么多个小时人们只是等着运行代码。如果是在运行时,欢迎JVM启动时间......

答案 4 :(得分:2)

因为生成语义正确的代码是编译器的工作。将String.Concat的调用更改为StringBuilder.Append的调用将改变代码的语义。

答案 5 :(得分:1)

我认为对于编译器编写者来说这有点太复杂了。当你在连接中引用循环内的中间字符串时(例如将它们传递给其他方法),这种优化是不可能的。

答案 6 :(得分:0)

可能因为在代码中匹配这样的模式很复杂,并且如果编译器由于某种原因无法进行匹配,代码的性能突然变得非常糟糕。像这样优化代码会鼓励编写这样的代码,这会进一步增加编译器无法再进行优化的负面影响。

为了连接一组已知的字符串,StringBuilder并不比String.Concat快。

答案 7 :(得分:-2)

String是一种不可变类型,因此使用串联字符串比使用StringBuilder.Append要慢。

修改:为了更清楚地说明我的观点,当您谈到为什么String.Concat未针对StringBuilder.Append进行优化时,StringBuilder类完全不同语义为String的不可变类型。为什么你期望编译器优化它,因为它们显然是两个不同的东西?此外,StringBuilder是一个可以动态改变其长度的可变类型,为什么编译器应该将不可变类型优化为可变类型?这就是.NET Framework的ECMA规范中根深蒂固的设计和语义,无论语言如何。

这有点像要求编译器(也许期望太多)编译char并将其优化为int,因为int工作在32位而不是8位会被认为更快!