ref 关键字如何工作(在内存方面)

How does the ref keyword work (in terms of memory)

C# 有一个 ref 关键字。使用 ref 可以通过引用将 int 传递给方法。当你调用一个通过引用接受 int 的方法时,堆栈帧上发生了什么?

public void SampleMethod(ref int i) { }

这是一个简单的 C# 代码示例:

void Main()
{
    int i = 1;
    inc(ref i);
    Console.WriteLine(i);
}

public void inc(ref int i) { 
  i++;
}

这里是生成的IL代码

IL_0000:  nop         
IL_0001:  ldc.i4.1    
IL_0002:  stloc.0     // i
IL_0003:  ldarg.0     
IL_0004:  ldloca.s    00 // i
IL_0006:  call        inc
IL_000B:  nop         
IL_000C:  ldloc.0     // i
IL_000D:  call        System.Console.WriteLine
IL_0012:  nop         
IL_0013:  ret         

inc:
IL_0000:  nop         
IL_0001:  ldarg.1     
IL_0002:  dup         
IL_0003:  ldind.i4    
IL_0004:  ldc.i4.1    
IL_0005:  add         
IL_0006:  stind.i4    
IL_0007:  ret     

请注意,对于这个简单的案例,实际上只有一个区别 ldloca.s 00 或 ldloc.0。加载本地或加载地址(偏移量 00)

这是最简单级别的差异(这是您在评论中要求的)——加载变量的值或加载变量的地址。事情很快就会变得复杂——如果你调用的函数不是本地的,如果你传递的变量不是本地的等等等等。但在基本层面上,这就是区别。

我使用 linqpad 快速完成 diss-assembly -- 我推荐它。 http://www.linqpad.net/

局部变量或字段的地址。在 IL 中,ldloca.s 指令用于局部变量。

Loads the address of the local variable at a specific index onto the evaluation stack

stind指令用于将值存回变量

Store value of type (...) into memory at address

地址为 32/64 位,具体取决于目标体系结构。

将局部变量作为引用传递

在低级别,引用的局部int变量将被放入堆栈(大多数情况下整数存储在寄存器中),并且指向堆栈的指针将传递给调用的函数(指针本身最有可能在寄存器中传递)。考虑以下示例:

var i = 7;
Console.WriteLine(i);
inc(ref i);
Console.WriteLine(i);

这将是 JIT-et 类似这样的东西(目标架构是 x86):

    17:             var i = 7;
    # allocate space on the stack for args and i
00482E3B  sub         esp,8  
    # initialize i to 0
00482E3E  xor         eax,eax  
00482E40  mov         dword ptr [ebp-8],eax  
    # args saved to stack (could be optimised out)  
00482E43  mov         dword ptr [ebp-4],ecx  
00482E46  cmp         dword ptr ds:[3ACAECh],0  
00482E4D  je          00482E54  
00482E4F  call        7399CB2D  
    # i = 7
00482E54  mov         dword ptr [ebp-8],7  
    18:             Console.WriteLine(i);
    # load the value of i into ecx, and call cw
00482E5B  mov         ecx,dword ptr [ebp-8]  
00482E5E  call        72E729DC  
    19:             inc(ref i);
    # load the address of i into ecx, and call inc
00482E63  lea         ecx,[ebp-8]  
00482E66  call        dword ptr ds:[4920860h]  
    20:             Console.WriteLine(i);
    # load the value of i into ecx, and call cw
00482E6C  mov         ecx,dword ptr [ebp-8]  
00482E6F  call        72E729DC  
    21:         }
00482E74  nop  
00482E75  mov         esp,ebp  
00482E77  pop         ebp  
00482E78  ret  

传递数组项或对象成员作为引用

这里发生了几乎相同的事情,获取字段或元素的地址,并将指针传递给函数:

var i = new[]{7};
Console.WriteLine(i[0]);
inc(ref i[0]);
Console.WriteLine(i[0]);

编译为(去掉无聊的部分):

    18:             Console.WriteLine(i[0]);
00C82E91  mov         eax,dword ptr [ebp-8]  
00C82E94  cmp         dword ptr [eax+4],0  
00C82E98  ja          00C82E9F  
00C82E9A  call        7399BDC2  
00C82E9F  mov         ecx,dword ptr [eax+8]  
00C82EA2  call        72E729DC  
    19:             inc(ref i[0]);
    # loading the reference of the array to eax
00C82EA7  mov         eax,dword ptr [ebp-8]  
    # array boundary check is inlined
00C82EAA  cmp         dword ptr [eax+4],0  
00C82EAE  ja          00C82EB5  
    # this would throw an OutOfBoundsException, but skipped by ja
00C82EB0  call        7399BDC2  
    # load the address of the element in ecx, and call inc
00C82EB5  lea         ecx,[eax+8]  
00C82EB8  call        dword ptr ds:[4F80860h]  

请注意,在这种情况下不必固定数组,因为 框架知道 ecx 中的地址指向数组中的一个项目, 因此,如果堆压缩发生在 leacall 之间或 inc 函数内部,它可以直接重新调整 ecx 的值。

您可以通过打开反汇编 window (Debug/Windows/Disassembly)Visual Studio 调试器自行研究 JIT-ed 程序集

它将通过引用传递局部变量,而不是为其发送新副本