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

Wim*_*dse 21 .net c# string clr optimization

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

现在使用只在运行时知道的字符串串联,为什么编译器不会优化循环中的字符串连接和多于10个字符串的串联来StringBuilder.Append代替?我的意思是,这是可能的,对吧?实例化a StringBuilder并进行每个连接并将其转换为Append()呼叫.

是否有任何理由为什么这应该可以 进行优化?我错过了什么?

LBu*_*kin 41

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

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

string s = "";
for( int i = 0; i < 100; i ++ )
    s = string.Concat( s, i.ToString() );
Run Code Online (Sandbox Code Playgroud)

进入这个:

StringBuilder sb = new StringBuilder();
for( int i = 0; i < 100; i++ )
    sb.Append( i.ToString() );
string s = sb.ToString();
Run Code Online (Sandbox Code Playgroud)

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

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

如果您的循环条件更复杂,如果编译器尝试执行某些静态分析以确定这种重写的结果是否仍然在功能上相同,那该怎么办?在许多方面,这就像解决暂停问题.

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

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

  • @BluRaja,如果可以的话,我会为你做的.编译器负责*平台特定的微优化*.开发人员负责选择正确的算法.在这种情况下,String.Concat和StringBuilder之间存在明确的内存/碎片/时间权衡 - 只有开发人员才能根据预期的数据进行调用. (7认同)
  • @BlueRaja:许多性能故障是由于算法和数据结构选择不当造成的.您是否希望编译器也以这种方式重写代码?正如我在我的回答中所说,编译器可以在其中进行*安全,不变保留*优化.除此之外,您作为开发人员有责任:1)确定性能的重要性,2)设计您的系统以针对这些情况采取最佳行为,以及3)在出现性能问题时分析和优化实施. (6认同)
  • -1 for*"作为开发人员,你的工作就是编写性能良好的代码,"*责任应该*尽可能*在可能的情况下推送到编译器上. (5认同)
  • 我认为*可能*之类的东西之间存在差异,而且*值得*.在一些狭隘的用途中,有可能用`StringBuilder`(或类似的方法)来代替字符串的串联.但在实践中,由于并不总是可能,并且在某些情况下可能会带来负面后果 - 因此作为一般优化可能不是一个好主意.顺便说一句,我很好奇是否有人知道任何Java编译器**是否实际执行了上述链接描述的字符串优化? (2认同)

Eri*_*ert 30

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

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

由于那些程序员可能不知道天真字符串分配的n平方成本,可能不知道字符串构建器的存在,并且经常使用这种模式编写代码,我们认为将此优化放入JScript是合理的.净.

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

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


Jon*_*eet 16

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

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

  • C#编译器实际上将`a + b + c + d + e`编译为`string.Concat(a,b,c,d,e)`.实际上有一个带有4个参数的"Concat"版本中的一个错误导致了一些疯狂的问题:https://connect.microsoft.com/VisualStudio/feedback/details/361125/ (7认同)

Sam*_*ell 6

两个原因:

  • 您无法以编程方式识别其性能严格较高的位置.
  • 如果执行不正确,"优化"将减慢速度.

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

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

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