7

简而言之,这是来自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是在哪里转换为字符串?幕后究竟发生了什么?本章的其余部分没有更多解释。

4

3 回答 3

15

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

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

string.Concat函数内部,它调用传入ToString()的两个objects。此语句中的关键性能问题是(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 在评论中提出的问题:

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

基本上,他们必须为表面上在strings 上运行但在其中提供重载的函数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)?”。

于 2013-08-18T01:29:42.630 回答
4

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

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

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

这是第一个版本的功能:

sb.Append ( i+",");
  1. 打电话i.ToString
  2. 创建一个新的string(想想new string(iAsString + ","))。
  3. 调用 sb.Append。

这是第二个版本的作用:

  1. 打电话i.ToString
  2. 打电话sb.Append
  3. 打电话sb.Append

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

于 2013-08-17T22:39:30.333 回答
0

When you do the following:

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

the second line actual ends up abandoning the first string valued with "abc" and creates a new string for x="abcd"; I think that is the performance hit you are seeing.

于 2013-08-17T22:48:23.053 回答