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

本文关键字:转换 字符串 int 文本 ToString 为什么 | 更新日期: 2023-09-27 18:27:59

这是来自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被转换为字符串?这里到底在做什么?本章的其余部分没有更多的解释。

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

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

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

string.Concat 函数内部,它对传入的两个object调用ToString()。 此声明中的关键性能问题是(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调用,最后以 Call 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()是个好主意。

在评论中回答西奥多罗斯的问题:

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

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

在我看来,这似乎有很多复杂性和分析,以便对一些字符串操作函数进行微小的优化。 此外,这将与核心编译器函数解析代码混淆,该代码已经有一些人们一直误解的非常精确的规则(看看 Eric Lippert 的一些答案 - 相当多的答案围绕着函数解析问题(。 如果回报很小,那么使用"它像这样工作,除非你有这种情况"类型的规则使其更加复杂,这当然是要避免的。

更便宜和不太复杂的解决方案是使用基本函数解析规则,并让编译器解析将值类型(如int(传递到函数中,并让它弄清楚唯一适合它的函数签名是采用object的签名,并做一个盒子。 然后依靠用户在分析代码并确定有必要时对ToString()进行优化(或者只是知道这种行为并在遇到情况时一直这样做,我就是这样做的(。

他们本可以做的更可能的替代方案是有许多string.Concat重载,这些重载需要 int s、double 等(如 string.Concat(int, int) (,并且只需在内部调用 ToString 参数,它们不会被框住。 这样做的好处是优化是在类库中而不是编译器中,但是您不可避免地会遇到想要在串联中混合类型的情况,例如您在此处string.Concat(int, string)的原始问题。 排列会爆炸,这可能是他们没有这样做的原因。 他们也可以确定使用这种重载的最常见情况并排在前 5 名,但我猜他们决定这样做只会让他们问"好吧,你做了(int, string),你为什么不做(string, int)

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

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

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

以下是第一个版本的作用:

sb.Append ( i+",");
  1. 呼叫i.ToString
  2. 创建一个新string(new string(iAsString + ",")想(。
  3. 呼叫 sb。附加。

以下是第二个版本的作用:

  1. 呼叫i.ToString .
  2. 呼叫sb.Append .
  3. 呼叫sb.Append

如您所见,唯一的区别是第二步,在第二个版本中调用sb.Append应该比连接两个字符串并从结果创建另一个实例更快。

当您执行以下操作时:

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

第二行实际上最终放弃了第一个以"abc"值的字符串,并为x="abcd"创建了一个新字符串; 我认为这就是你所看到的性能打击。