从IL看来,C#实际上并没有复制传递参数的整个“大结构”吗?

时间:2018-04-19 03:31:50

标签: c# compilation

我经常听说使用值类型时我们必须小心将它们传递给方法,因为值将被复制。如果结构很大,则可能是一个问题。

但是我看了IL的代码:

using System;

public class Program
{
    public struct Big
    {
        public int a, b, c, d;
    }

    public static void Main()
    {
        Big big = new Big();
        UseBig(big);
        big.a += 7;
    }

    private static void UseBig(Big big)
    {
        big.a += 1;
        big.b += 1;
    }
}

IL:

.method public hidebysig static void  Main() cil managed
  {
    // 
    .maxstack  3
    .locals init (valuetype Program/Big V_0)
    IL_0000:  nop
    IL_0001:  ldloca.s   V_0
    IL_0003:  initobj    Program/Big
    IL_0009:  ldloc.0
    IL_000a:  call       void Program::UseBig(valuetype Program/Big)
    IL_000f:  nop
    IL_0010:  ldloca.s   V_0
    IL_0012:  dup
    IL_0013:  ldfld      int32 Program/Big::a
    IL_0018:  ldc.i4.7
    IL_0019:  add
    IL_001a:  stfld      int32 Program/Big::a
    IL_001f:  ret
  } // end of method Program::Main
.method private hidebysig static void  UseBig(valuetype Program/Big big) cil managed
  {
    // 
    .maxstack  8
    IL_0000:  nop
    IL_0001:  ldarga.s   big
    IL_0003:  dup
    IL_0004:  ldfld      int32 Program/Big::a
    IL_0009:  ldc.i4.1
    IL_000a:  add
    IL_000b:  stfld      int32 Program/Big::a
    IL_0010:  ldarga.s   big
    IL_0012:  dup
    IL_0013:  ldfld      int32 Program/Big::b
    IL_0018:  ldc.i4.1
    IL_0019:  add
    IL_001a:  stfld      int32 Program/Big::b
    IL_001f:  ret
  } // end of method Program::UseBig

调用函数的机制是它load location of local variable #0到堆栈,然后在它使用的函数load address of argument内。然后我们可以使用此地址load fieldstore fielddup因为loadstore会使用堆栈顶部的地址。

使用ref进行另一次运行:

using System;

public class Program
{
    public struct Big
    {
        public int a, b, c, d;
    }

    public static void Main()
    {
        Big big = new Big();
        UseBig(ref big);
        big.a += 7;
    }

    private static void UseBig(ref Big big)
    {
        big.a += 1;
        big.b += 1;
    }
}

IL:

.method public hidebysig static void  Main() cil managed
  {
    // 
    .maxstack  3
    .locals init (valuetype Program/Big V_0)
    IL_0000:  nop
    IL_0001:  ldloca.s   V_0
    IL_0003:  initobj    Program/Big
    IL_0009:  ldloca.s   V_0
    IL_000b:  call       void Program::UseBig(valuetype Program/Big&)
    IL_0010:  nop
    IL_0011:  ldloca.s   V_0
    IL_0013:  dup
    IL_0014:  ldfld      int32 Program/Big::a
    IL_0019:  ldc.i4.7
    IL_001a:  add
    IL_001b:  stfld      int32 Program/Big::a
    IL_0020:  ret
  } // end of method Program::Main
.method private hidebysig static void  UseBig(valuetype Program/Big& big) cil managed
  {
    // 
    .maxstack  8
    IL_0000:  nop
    IL_0001:  ldarg.0
    IL_0002:  dup
    IL_0003:  ldfld      int32 Program/Big::a
    IL_0008:  ldc.i4.1
    IL_0009:  add
    IL_000a:  stfld      int32 Program/Big::a
    IL_000f:  ldarg.0
    IL_0010:  dup
    IL_0011:  ldfld      int32 Program/Big::b
    IL_0016:  ldc.i4.1
    IL_0017:  add
    IL_0018:  stfld      int32 Program/Big::b
    IL_001d:  ret
  } // end of method Program::UseBig

在输入函数时,它改为使用load local variable address,因此地址在函数内部而不是它的值,而不是直接加载它的地址我们load argument值,因为它已经是一个地址。我们在这里所做的一切都将影响调用者的值,因为它是对调用者的地址而不是参数变量空间。因此,这是ref的行为。

问题是:

  1. 实际上它并不依赖于structcd)中的任何其他变量?无论它有多大,程序实际上都将整个结构作为单个地址传递。那么人们一直在谈论的成本在哪里呢?
  2. 引擎盖ref实际上没有保存任何“结构复制成本”?
  3. 这些IL代码是从https://dotnetfiddle.net/

    获得的

1 个答案:

答案 0 :(得分:2)

在第一个版本中,副本由

生成
ldloc.0

没有传递给该函数的地址。

无论如何,你不能依赖IL检查,尝试查看生成的本机代码。即使在第一种情况下,内联可能会发生并优化掉该副本。