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

时间:2013-08-17 22:36:44

标签: c#

这是来自 C#简而言之一书

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

//Outputs 0,1,2,3.............49,

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

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

for(int i = 0; i < 50; i++) {
    sb.Append(i.ToString()); 
    sb.Append(",");
}

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

3 个答案:

答案 0 :(得分:15)

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

StringBuilder.Append(string.Concat((object)i, (object)","));

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)

看到第一行是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()来电,这相对便宜得多。

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

你可以将这个想法扩展到许多其他的东西 - 在你正在将值类型传递给未明确将该类型作为参数的函数的字符串上操作的任何地方(调用带有object的调用作为参数,例如string.Format(),在传递值类型参数时调用<valuetype>.ToString()是个好主意。

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

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

基本上,他们必须为表面上在string上运行的函数分配特殊情况,但在其中提供object的重载(基本上,if (boxing occurs && overload has string)) 。在该分支内部,编译器还必须检查以验证object函数重载是否与string重载相同,除了在参数上调用ToString()之外 - 它需要这样做是因为用户可以创建函数重载,其中一个函数占用string而另一个函数占用object,但是这两个重载对参数执行不同的工作。

在我看来,对于对一些字符串操作函数进行微小优化,我感觉很复杂和分析。此外,这将与核心编译器功能解析代码混淆,核心编译器功能解析代码已经有一些人们一直误解的非常精确的规则(看看Eric Lippert的一些答案 - 很多都围绕功能解决问题)。使它变得更复杂“它就像这样工作,除非你有这种情况”类型规则肯定是要避免的,如果返回是最小的。

更便宜和更简单的解决方案是使用基本功能解析规则,让编译器解决您将值类型(如int)传入函数,并让它弄清楚只有适合它的函数签名才能使用object并执行一个框。然后依靠用户在他们对代码进行概要分析时进行ToString()的优化,并确定它是必要的(或者只是知道这种行为,并且当他们遇到这种情况时,无论如何都会这样做。) / p>

他们可能做的更有可能的替代方案是,string.Concat s,int等需要double次重载(例如string.Concat(int, int))并且只是在内部的参数上调用ToString,它们不会被装箱。这样做的优点是优化是在类库而不是编译器中,但是你不可避免地会遇到想要在串联中混合类型的情况,就像你在string.Concat(int, string)的原始问题一样。排列会爆炸,这可能是他们没有这样做的原因。他们也可以确定最常用的情况,这些超载会被使用并进入前5名,但我猜他们决定只打开那些问“好吧,你做过(int, string),为什么你做不到(string, int)?“。

答案 1 :(得分:4)

  

现在我们有一个额外的步骤,我将被转换为字符串?

这不是一个额外的步骤。即使在第一个片段中,显然整数i必须转换为字符串某处 - 这是由加法运算符处理的,所以它发生在你看不到它的地方,但它仍然会发生。

第二个代码段更快的原因是因为它不必通过连接i.ToString()","的结果来创建新字符串。

这是第一个版本的作用:

sb.Append ( i+",");
  1. 致电i.ToString
  2. 制作新的string(想想new string(iAsString + ","))。
  3. 致电sb.Append。
  4. 以下是第二个版本的作用:

    1. 致电i.ToString
    2. 致电sb.Append
    3. 致电sb.Append
    4. 正如您所看到的,唯一的区别是第二步,其中第二个版本中调用sb.Append的速度要快于连接两个字符串并从结果中创建另一个实例。

答案 2 :(得分:0)

执行以下操作时:

string x = "abc";
x = x + "d";     // or even x += "d";

第二行实际结束时放弃了第一个用“abc”值的字符串,并为x =“abcd”创建一个新字符串;我认为这是你所看到的性能打击。