C#:为什么.ToString()将文本更快地附加到转换为字符串的int?

iAt*_*_it 7 c#

这是一本简单的书中的C#

StringBuilder sb = new StringBuilder();
for(int i = 0; i < 50; i++) 
     sb.Append (i + ",");

//Outputs 0,1,2,3.............49,
Run Code Online (Sandbox Code Playgroud)

然而,它然后说"表达式i +","意味着我们仍然反复连接字符串,但这只会因为字符串很小而导致性能成本很低"

然后它说将它改成下面的行使它更快

for(int i = 0; i < 50; i++) {
    sb.Append(i.ToString()); 
    sb.Append(",");
}
Run Code Online (Sandbox Code Playgroud)

但为什么这会更快?现在我们有一个额外的步骤,i正在转换为字符串?这里有什么实际内容?本章其余部分没有任何解释.

Gje*_*ema 15

你的问题的前两个答案并不完全正确.该sb.Append(i + ",");语句不调用i.ToString(),它实际上做的是

StringBuilder.Append(string.Concat((object)i, (object)","));
Run Code Online (Sandbox Code Playgroud)

string.Concat函数内部,它调用传入ToString()的两个函数object.本声明中的关键性能问题是(object)i.这是装箱 - 在引用中包装值类型.这是一个(相对)相当大的性能命中,因为它需要额外的周期和内存分配来装箱,然后需要额外的垃圾收集.

您可以在(发布)编译代码的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)
Run Code Online (Sandbox Code Playgroud)

看到第一行是一个box调用,然后是一个Concat调用,以finally调用结束Append.

如果你打电话i.ToString(),如下图所示,你放弃了拳击,还有string.Concat()通话.

for (int i = 0; i < 50; i++)
{
    sb.Append(i.ToString());
    sb.Append(",");
}
Run Code Online (Sandbox Code Playgroud)

此调用产生以下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)
Run Code Online (Sandbox Code Playgroud)

请注意,没有拳击,也没有String.Concat,因此创建的资源较少,需要收集的资源较少,而且在装箱时浪费的周期较少,代价是添加一个Append()呼叫,这相对便宜得多.

这就是为什么第二组代码具有更好的性能.

您可以将这个想法扩展到许多其他的事情-任何地方,对您传递一个值类型为未明确采取这一类型作为参数的函数的字符串操作(调用,需要一个object作为参数,像string.Format()例如) ,<valuetype>.ToString()传递值类型参数时调用是个好主意.

回应Theodoros在评论中提出的问题:

编译团队当然可以决定进行这样的优化,但我的猜测是他们认为成本(在额外的复杂性,时间,额外的测试等方面)使得这种变化的价值不值得投资.

基本上,他们不得不在特殊情况下为表面上可操作的函数分支string,但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)?".