C# 到 CIL 装箱与 ToString 成本

Posted

技术标签:

【中文标题】C# 到 CIL 装箱与 ToString 成本【英文标题】:C# to CIL Boxing vs. ToString Cost 【发布时间】:2015-05-13 18:37:54 【问题描述】:

我正在阅读《CLR via C#》(第 4 版)一书,不是作为 C# 的新手,而是作为一个了解该语言的人,试图提高我对 CLR 底层功能的掌握。

无论如何,在本书中给出了一个示例(pg127-131),该示例讨论了值类型的装箱/拆箱,该示例以对 Console.WriteLine 的调用结束,其中值类型连接到作为参数传递的字符串。

这本书解释了装箱和拆箱/复制操作会导致开销,我已经知道了,但随后说明可以通过在传入的值类型上运行 .ToString() 来优化示例。

我创建了一个示例程序并对其进行了编译,然后使用 ILDASM 检查它生成的 IL。带有 ToString 的版本本质上是相同的,但是用对 ToString 的“调用”替换了“box”指令(这并不令人震惊)。

我在 100000 次运行的循环中对代码进行了基准测试,没有任何区别(它会波动哪个更快)。我意识到在进行基准测试(缓存等)时还有其他因素会起作用,但是按照本书的解释,即使在幼稚的基准测试中,我也希望在避免“盒子”指令时看到显着的差异..

仅仅是调用一个函数不是更好吗? ToString 中是否正在进行装箱操作,使好处无效并且这本书是错误的?有人可以对此有所了解吗?

作为参考,以下是两个 ILDASM 读数:


.method private hidebysig static void  Main(string[] args) cil managed

  .entrypoint
  // Code size       24 (0x18)
  .maxstack  2
  .locals init (int32 V_0)
  IL_0000:  ldc.i4.4
  IL_0001:  stloc.0
  IL_0002:  ldloc.0
  IL_0003:  box        [mscorlib]System.Int32
  IL_0008:  ldstr      "."
  IL_000d:  call       string [mscorlib]System.String::Concat(object,
                                                              object)
  IL_0012:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_0017:  ret
 // end of method Program::Main

.method private hidebysig static void  Main(string[] args) cil managed

  .entrypoint
  // Code size       25 (0x19)
  .maxstack  2
  .locals init (int32 V_0)
  IL_0000:  ldc.i4.4
  IL_0001:  stloc.0
  IL_0002:  ldloca.s   V_0
  IL_0004:  call       instance string [mscorlib]System.Int32::ToString()
  IL_0009:  ldstr      "."
  IL_000e:  call       string [mscorlib]System.String::Concat(string,
                                                              string)
  IL_0013:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_0018:  ret
 // end of method Program::Main

【问题讨论】:

看反汇编窗口,看看x86汇编代码有什么区别。 【参考方案1】:

CLR 可能会内联对string.Concat(object,object) 的调用,这会产生与您的“优化”版本相同的代码。请注意,C# 编译器会将很多此类优化留给 CLR,因为它有更好的工具来执行这些优化。

除了几个空检查(将被优化)之外,它只调用string.Concat(left.ToString(),right.ToString()),这将被简化为string.Concat(left,right.ToString()),因为CLR 会看到ToString() 只返回this

因此,两种情况下的执行代码可能相同。

【讨论】:

有道理 - 我过于关注 IL。【参考方案2】:

您错过了这样一个事实,即String.Concat 将在内部提供object 参数上调用ToString

public static String Concat(Object arg0, Object arg1) 
    Contract.Ensures(Contract.Result<String>() != null);
    Contract.EndContractBlock();

    if (arg0 == null)
    
        arg0 = String.Empty;
    

    if (arg1==null) 
        arg1 = String.Empty;
    
    return Concat(arg0.ToString(), arg1.ToString());

所以call 指令无论如何都会在那里,但它在Concat 方法调用中对你隐藏。

调用ToString 会选择不同的Concat 重载,而这个不会在内部调用ToString

IL_000d:  call       string [mscorlib]System.String::Concat(object, object)

对比

IL_000e:  call       string [mscorlib]System.String::Concat(string, string)

【讨论】:

呼叫一直存在,但有一个额外的装箱操作。 @usr 是的,这正是正在发生的事情。

以上是关于C# 到 CIL 装箱与 ToString 成本的主要内容,如果未能解决你的问题,请参考以下文章

从 c# 表达式中删除不需要的装箱转换

c#装箱与拆箱

为啥这个非常简单的 C# 方法会产生如此不合逻辑的 CIL 代码?

《精通C#》第18章-CIL和动态程序集的作用

C#中的泛型是啥意思?

如何查看 C# 编译器生成的 MSIL / CIL?为啥叫组装?