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
中的地址指向数组中的一个项目,
因此,如果堆压缩发生在 lea
和 call
之间或 inc 函数内部,它可以直接重新调整 ecx
的值。
您可以通过打开反汇编 window (Debug/Windows/Disassembly)Visual Studio 调试器自行研究 JIT-ed 程序集
它将通过引用传递局部变量,而不是为其发送新副本
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
中的地址指向数组中的一个项目,
因此,如果堆压缩发生在 lea
和 call
之间或 inc 函数内部,它可以直接重新调整 ecx
的值。
您可以通过打开反汇编 window (Debug/Windows/Disassembly)Visual Studio 调试器自行研究 JIT-ed 程序集
它将通过引用传递局部变量,而不是为其发送新副本