您问题的前两个答案并不完全正确。该sb.Append(i + ",");语句没有调用i.ToString(),它实际上所做的是
StringBuilder.Append(string.Concat((object)i, (object)","));
在string.Concat函数内部,它调用传入ToString()的两个objects。此语句中的关键性能问题是(object)i. 这是装箱 - 在引用中包装一个值类型。这是(相对)相当大的性能损失,因为它需要额外的周期和内存分配来装箱,然后需要额外的垃圾收集。
您可以在 (Release) 编译代码的 IL 中看到这种情况:
IL_000c: box [mscorlib]System.Int32
IL_0011: ldstr ","
IL_0016: call string [mscorlib]System.String::Concat(object,
object)
IL_001b: callvirt instance class [mscorlib]System.Text.StringBuilder
[mscorlib]System.Text.StringBuilder::Append(string)
看到第一行是一个box调用,后面是一个Concat调用,最后是调用Append。
如果你i.ToString()改为打电话,如下所示,你放弃了拳击,也放弃了string.Concat()电话。
for (int i = 0; i < 50; i++)
{
sb.Append(i.ToString());
sb.Append(",");
}
此调用产生以下 IL:
IL_000b: ldloca.s i
IL_000d: call instance string [mscorlib]System.Int32::ToString()
IL_0012: callvirt instance class [mscorlib]System.Text.StringBuilder
[mscorlib]System.Text.StringBuilder::Append(string)
IL_0017: pop
IL_0018: ldloc.0
IL_0019: ldstr ","
IL_001e: callvirt instance class [mscorlib]System.Text.StringBuilder
[mscorlib]System.Text.StringBuilder::Append(string)
请注意,没有装箱,也没有String.Concat,因此需要收集的创建的资源更少,并且在装箱上浪费的周期更少,代价是增加了一个Append()调用,相对便宜得多。
这就是为什么第二组代码性能更好的原因。
您可以将此想法扩展到许多其他事情 - 任何对字符串进行操作的地方,您将值类型传递给未明确将该类型作为参数的函数(例如,将 aobject作为参数的调用string.Format()) ,<valuetype>.ToString()在传入值类型参数时调用是个好主意。
针对 Theodoros 在评论中提出的问题:
编译器团队当然可以决定进行这样的优化,但我的猜测是他们认为成本(在额外的复杂性、时间、额外的测试等方面)使得这样的改变的价值不值得投资。
基本上,他们必须为表面上在strings 上运行但在其中提供重载的函数object(基本上,if (boxing occurs && overload has string))进行特殊情况分支。在该分支内,编译器还必须检查以验证object函数重载是否与重载执行相同的操作,string但调用ToString()参数除外 - 它需要这样做,因为用户可以创建函数重载,其中一个函数采用string另一个需要一个object,但是这两个重载对参数执行不同的工作。
在我看来,这对于对一些字符串操作函数进行小优化需要很多复杂性和分析。此外,这将与核心编译器函数解析代码混在一起,它已经有一些人们一直误解的非常精确的规则(看看 Eric Lippert 的一些答案 - 不少围绕函数解析问题)。如果回报是最小的,那么使用“它像这样工作,除非你有这种情况”类型规则使它变得更加复杂,当然是要避免的。
较便宜且不太复杂的解决方案是使用基本函数解析规则,并让编译器解析您将值类型(如int)传递给函数,并让它找出适合它的唯一函数签名是一个这需要object,并做一个盒子。然后依靠用户对ToString()他们何时分析他们的代码并确定是否有必要进行优化(或者只是知道这种行为并在遇到这种情况时一直这样做,我就是这样做的)。
他们可以做的一个更可能的替代方案是有一些string.Concat重载,它们采用ints、doubles 等(如string.Concat(int, int)),并且只ToString在内部调用它们不会被装箱的参数。这样做的好处是优化是在类库中而不是编译器中,但是你不可避免地会遇到想要在连接中混合类型的情况,就像你有的原始问题一样string.Concat(int, string)。排列会爆炸,这可能是他们没有这样做的原因。他们还可以确定使用此类重载的最常用情况并排在前 5 位,但我猜他们决定将它们开放给人们问“好吧,你做到了(int, string),你为什么不做(string, int)?”。