为什么String.Concat没有针对StringBuilder.Append进行优化?
我发现常量字符串表达式的连接由编译器优化为一个字符串。
现在使用只在运行时知道字符串的字符串连接,为什么编译器不优化循环中的字符串连接和超过10个字符串的连接以使用StringBuilder.Append
? 我的意思是,这是可能的,对吗? 实例化StringBuilder
并进行每个连接并将其转换为Append()
调用。
有什么理由应该或不能优化? 我错过了什么?
明确的答案必须来自编译器设计团队。 但是让我在这里捅一下……
如果你的问题是,编译器为什么不转这个:
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。 如果明天会有更优化的字符串组装服务怎么办? 编译器应该使用它吗?
如果您的循环条件更复杂 ,如果编译器尝试执行某些静态分析以确定这种重写的结果是否仍然在function上等效,该怎么办? 在许多方面,这就像解决停止问题 。
最后,我不确定在所有情况下这都会导致代码执行速度更快。 在附加文本时,实例化StringBuilder
并调整其内部缓冲区的大小是成本。 事实上,追加的成本与被连接的字符串的大小,有多少,内存压力是什么有很大关系。 这些是编译器无法提前预测的内容。
编写性能良好的代码是开发人员的工作。 编译器只能通过进行某些安全 ,不变的保留优化来提供帮助。 不要为您重写代码。
LBuskin的答案非常好; 我只想补充几件事。
首先,JScript.NET确实进行了这种优化。 JScript经常被经验较少的程序员用于涉及在循环中构造大字符串的任务,例如构建JSON对象,HTML数据等。
由于那些程序员可能不知道天真字符串分配的n平方成本,可能不知道字符串构建器的存在,并经常使用这种模式编写代码,我们认为将此优化放入JScript是合理的。净。
C#程序员倾向于更多地意识到他们编写的代码的底层成本,并且更多地意识到StringBuilder等现成部件的存在,因此他们需要更少的优化。 更重要的是,C#的设计理念是它是一种“做我说的”语言,至少具有“魔力”; JScript是一种“尽我所能”的语言,它尽力弄清楚如何最好地为您服务,即使这意味着有时候猜错了。 这两种哲学都是有效的。
有时它会“走另一条路”。 将此选项与我们为字符串切换所做的选择进行比较。 对字符串的切换实际上是编译为包含字符串的字典的创建,而不是作为一系列字符串比较。 这种优化可能很糟糕; 简单地进行字符串比较可能会更快。 但是在这里我们猜测你“意味着”转换为表查找而不是一系列“if”语句 – 如果你的意思是if语句系列,你可以自己轻松地写出来。
对于多个字符串的单个串联(例如a + b + c + d + e + f + g + h + i + j),您确实希望使用String.Concat
IMO。 它有为每个调用构建一个数组的开销,但它的好处是该方法可以在需要分配任何内存之前计算出结果字符串的确切长度。 StringBuilder.Append(a).Append(b)...
只提供一个值,因此构建器不知道要分配多少内存。
至于在循环中执行它 – 此时你已经添加了一个新的局部变量,并且你必须添加代码以在恰当的时间写回字符串变量(调用StringBuilder.ToString()
)。 当你在调试器中运行时会发生什么? 如果不看到价值积累,只是在循环结束时变得可见,难道不会让人感到困惑吗? 哦,当然你必须进行适当的validation,在循环结束之前的任何时候都不使用该值…
两个原因:
- 您无法以编程方式识别严格要求较高的位置。
- 如果执行不正确,“优化”将减慢速度。
您可以建议人们对他们的应用程序使用正确的调用,但在某些时候,开发人员有责任正确使用它。
编辑:关于截止,我们还有另外几个问题:
- 确保达到截止值的唯一方法是复杂的流量分析。 能够找到可以转换的部分的地方数量非常少。
- 流量分析很昂贵。 如果你在运行时这样做,整个程序将运行得更慢,因为很少有机会编写一段写得不好的代码。 如果你在编译时这样做,根据语言语法不是错误 ,但你可以发出警告 – 这正是FXCop所做的(一个缓慢但可用的流分析工具)。 试想一下FXCop是否总是必须与编译器一起运行; 这么多个小时人们只是在等待运行代码。 如果是在运行时,欢迎JVM启动时间……
因为生成语义正确的代码是编译器的工作。 将String.Concat
的调用更改为StringBuilder.Append
调用将改变代码的语义。
我相信对于编译器编写者来说这有点太复杂了。 当你在连接之外引用循环内的中间字符串时(例如将它们传递给其他方法),这种优化是不可能的。
可能因为在代码中匹配这样的模式很复杂,并且如果编译器由于某种原因无法进行匹配,代码的性能突然变得非常糟糕。 像这样优化代码会鼓励编写这样的代码,这甚至会进一步增加编译器无法再进行优化的负面影响。
为了连接一组已知的字符串, StringBuilder
并不比String.Concat
快。
String是一个不可变类型,因此使用串联字符串比使用StringBuilder.Append
要慢。
编辑:为了更清楚地说明我的观点,当你谈到为什么String.Concat
没有针对StringBuilder.Append
进行优化时, StringBuilder
类与不可变类型的String
具有完全不同的语义。 为什么你期望编译器优化它,因为它们显然是两个不同的东西? 此外, StringBuilder
是一个可以动态改变其长度的可变类型,为什么编译器应该将不可变类型优化为可变类型? 这就是.NET Framework的ECMA规范中根深蒂固的设计和语义,无论语言如何。
这有点像要求编译器(也许期望太多)编译一个char
并将其优化为一个int
因为int
工作在32位而不是8位,并且会被认为更快!