您问题的前两个答案并不完全正确。该sb.Append(i + ",");
语句没有调用i.ToString()
,它实际上所做的是
StringBuilder.Append(string.Concat((object)i, (object)","));
在string.Concat
函数内部,它调用传入ToString()
的两个object
s。此语句中的关键性能问题是(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 在评论中提出的问题:
编译器团队当然可以决定进行这样的优化,但我的猜测是他们认为成本(在额外的复杂性、时间、额外的测试等方面)使得这样的改变的价值不值得投资。
基本上,他们必须为表面上在string
s 上运行但在其中提供重载的函数object
(基本上,if (boxing occurs && overload has string)
)进行特殊情况分支。在该分支内,编译器还必须检查以验证object
函数重载是否与重载执行相同的操作,string
但调用ToString()
参数除外 - 它需要这样做,因为用户可以创建函数重载,其中一个函数采用string
另一个需要一个object
,但是这两个重载对参数执行不同的工作。
在我看来,这对于对一些字符串操作函数进行小优化需要很多复杂性和分析。此外,这将与核心编译器函数解析代码混在一起,它已经有一些人们一直误解的非常精确的规则(看看 Eric Lippert 的一些答案 - 不少围绕函数解析问题)。如果回报是最小的,那么使用“它像这样工作,除非你有这种情况”类型规则使它变得更加复杂,当然是要避免的。
较便宜且不太复杂的解决方案是使用基本函数解析规则,并让编译器解析您将值类型(如int
)传递给函数,并让它找出适合它的唯一函数签名是一个这需要object
,并做一个盒子。然后依靠用户对ToString()
他们何时分析他们的代码并确定是否有必要进行优化(或者只是知道这种行为并在遇到这种情况时一直这样做,我就是这样做的)。
他们可以做的一个更可能的替代方案是有一些string.Concat
重载,它们采用int
s、double
s 等(如string.Concat(int, int)
),并且只ToString
在内部调用它们不会被装箱的参数。这样做的好处是优化是在类库中而不是编译器中,但是你不可避免地会遇到想要在连接中混合类型的情况,就像你有的原始问题一样string.Concat(int, string)
。排列会爆炸,这可能是他们没有这样做的原因。他们还可以确定使用此类重载的最常用情况并排在前 5 位,但我猜他们决定将它们开放给人们问“好吧,你做到了(int, string)
,你为什么不做(string, int)
?”。