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
被转换为字符串?这里到底在做什么?本章的其余部分没有更多的解释。
你的问题的前两个答案不太正确。 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+",");
- 呼叫
i.ToString
。 - 创建一个新
string
(new string(iAsString + ",")
想(。 - 呼叫 sb。附加。
以下是第二个版本的作用:
- 呼叫
i.ToString
. - 呼叫
sb.Append
. - 呼叫
sb.Append
。
如您所见,唯一的区别是第二步,在第二个版本中调用sb.Append
应该比连接两个字符串并从结果创建另一个实例更快。
当您执行以下操作时:
string x = "abc";
x = x + "d"; // or even x += "d";
第二行实际上最终放弃了第一个以"abc"值的字符串,并为x="abcd"创建了一个新字符串; 我认为这就是你所看到的性能打击。