我目前正在使用 Profiler API 检查 CLR 中的深层对象。我在分析迭代器/异步方法的“this”参数时遇到了一个特定问题(由编译器生成,格式为<name>d__123::MoveNext
)。
在研究这个时,我发现确实有一种特殊的行为。首先,C# 编译器将这些生成的方法编译为结构(仅在发布模式下)。ECMA-334(C# 语言规范,第 5 版:https ://www.ecma-international.org/publications/files/ECMA-ST/ECMA-334.pdf)状态(12.7.8 此访问):
“...如果方法或访问器是迭代器或异步函数,则 this 变量表示为其调用方法或访问器的结构的副本,...。”
这意味着与其他“this”参数不同,在这种情况下“this”是按值发送的,而不是通过引用发送的。我确实看到副本没有在外面修改。我试图了解结构实际上是如何发送的。
我冒昧地剥离了复杂的案例,并用一个小结构复制了它。看下面的代码:
struct Struct
{
public static void mainFoo()
{
Struct st = new Struct();
st.a = "String";
st.p = new Program();
System.Console.WriteLine("foo: " + st.foo1());
System.Console.WriteLine("static foo: " + Struct.foo(st));
}
int i;
String a;
Program p;
[MethodImplAttribute(MethodImplOptions.NoInlining)]
public static int foo(Struct st)
{
return st.i;
}
[MethodImplAttribute(MethodImplOptions.NoInlining)]
public int foo1()
{
return i;
}
}
NoInlining
只是为了我们可以正确检查 JITted 代码。我正在研究三个不同的东西:mainFoo 如何调用 foo/foo1、foo 是如何编译的以及 foo1 是如何编译的。以下是生成的 IL 代码(使用 ildasm):
.method public hidebysig static int32 foo(valuetype nitzan_multi_tester.Struct st) cil managed noinlining
{
// Code size 7 (0x7)
.maxstack 8
IL_0000: ldarg.0
IL_0001: ldfld int32 nitzan_multi_tester.Struct::i
IL_0006: ret
} // end of method Struct::foo
.method public hidebysig instance int32 foo1() cil managed noinlining
{
// Code size 7 (0x7)
.maxstack 8
IL_0000: ldarg.0
IL_0001: ldfld int32 nitzan_multi_tester.Struct::i
IL_0006: ret
} // end of method Struct::foo1
.method public hidebysig static void mainFoo() cil managed
{
// Code size 86 (0x56)
.maxstack 2
.locals init ([0] valuetype nitzan_multi_tester.Struct st)
IL_0000: ldloca.s st
IL_0002: initobj nitzan_multi_tester.Struct
IL_0008: ldloca.s st
IL_000a: ldstr "String"
IL_000f: stfld string nitzan_multi_tester.Struct::a
IL_0014: ldloca.s st
IL_0016: newobj instance void nitzan_multi_tester.Program::.ctor()
IL_001b: stfld class nitzan_multi_tester.Program nitzan_multi_tester.Struct::p
IL_0020: ldstr "foo: "
IL_0025: ldloca.s st
IL_0027: call instance int32 nitzan_multi_tester.Struct::foo1()
IL_002c: box [mscorlib]System.Int32
IL_0031: call string [mscorlib]System.String::Concat(object,
object)
IL_0036: call void [mscorlib]System.Console::WriteLine(string)
IL_003b: ldstr "static foo: "
IL_0040: ldloc.0
IL_0041: call int32 nitzan_multi_tester.Struct::foo(valuetype nitzan_multi_tester.Struct)
IL_0046: box [mscorlib]System.Int32
IL_004b: call string [mscorlib]System.String::Concat(object,
object)
IL_0050: call void [mscorlib]System.Console::WriteLine(string)
IL_0055: ret
} // end of method Struct::mainFoo
生成的汇编代码(仅相关部分):
foo/foo1:
mov eax,dword ptr [rcx+10h]
ret
fooMain (line 18):
mov rcx,offset mscorlib_ni+0x8aaf8 (00007ffc`37d6aaf8) (MT: System.Int32)
call clr+0x2510 (00007ffc`392f2510) (JitHelp: CORINFO_HELP_NEWSFAST)
mov rsi,rax
lea rcx,[rsp+40h]
call 00007ffb`d9db04e0 (nitzan_multi_tester.Struct.foo1(), mdToken: 000000000600000b)
mov dword ptr [rsi+8],eax
mov rdx,rsi
mov rcx,1DBCE383690h
mov rcx,qword ptr [rcx]
call mscorlib_ni+0x635bd0 (00007ffc`38315bd0) (System.String.Concat(System.Object, System.Object), mdToken: 000000000600054f)
mov rcx,rax
call mscorlib_ni+0x56d290 (00007ffc`3824d290) (System.Console.WriteLine(System.String), mdToken: 0000000006000b78)
fooMain (line 19):
mov rcx,offset mscorlib_ni+0x8aaf8 (00007ffc`37d6aaf8) (MT: System.Int32)
call clr+0x2510 (00007ffc`392f2510) (JitHelp: CORINFO_HELP_NEWSFAST)
mov rsi,rax
lea rcx,[rsp+28h]
mov rax,qword ptr [rsp+40h]
mov qword ptr [rcx],rax
mov rax,qword ptr [rsp+48h]
mov qword ptr [rcx+8],rax
mov eax,dword ptr [rsp+50h]
mov dword ptr [rcx+10h],eax
lea rcx,[rsp+28h]
call 00007ffb`d9db04d8 (nitzan_multi_tester.Struct.foo(nitzan_multi_tester.Struct), mdToken: 000000000600000a)
mov dword ptr [rsi+8],eax
mov rdx,rsi
mov rcx,1DBCE383698h
mov rcx,qword ptr [rcx]
call mscorlib_ni+0x635bd0 (00007ffc`38315bd0) (System.String.Concat(System.Object, System.Object), mdToken: 000000000600054f)
mov rcx,rax
call mscorlib_ni+0x56d290 (00007ffc`3824d290) (System.Console.WriteLine(System.String), mdToken: 0000000006000b78)
我们可以看到的第一件事是 foo 和 foo1 都生成相同的 IL 代码(以及相同的 JIT 汇编代码)。这是有道理的,因为最终我们只是使用第一个参数。我们看到的第二件事是 mainFoo 以不同的方式调用这两种方法(ldloc 与 ldloca)。由于 foo 和 foo1 都期望相同的输入,我希望 mainFoo 将发送相同的参数。这提出了3个问题
1)在堆栈上加载结构与在该堆栈上加载结构的地址到底是什么意思?我的意思是,大小大于 8 字节(64 位)的结构不能“坐在”堆栈上。
2) CLR 是否在用作“this”之前生成结构的副本(我们知道这是真的,根据 C# 规范)?这个副本存储在哪里?fooMain 程序集显示调用方法在其堆栈上生成副本。
3) 似乎按值和地址加载结构(ldarg/ldloc vs ldarga/ldloca)实际上加载了一个地址——对于第二组它只是在之前创建了一个副本。为什么?我在这里错过了什么吗?
4) 回到迭代器/异步 - foo/foo1 示例是否复制了迭代器和非迭代器结构的“this”参数之间的差异?为什么需要这种行为?创建副本似乎是浪费工作。动机是什么?
(此示例是使用 .Net framework 4.5 进行的,但使用 .Net framework 2 和 CoreCLR 也可以看到相同的行为)