了解结构"this"参数(特别是迭代器/异步)



我目前正在使用 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 代码(以及相同的 JITted 汇编代码)。这是有道理的,因为最终我们只是使用第一个参数。我们看到的第二件事是,mainFoo对这两种方法的称呼不同(ldloc vs ldloca)。由于 foo 和 foo1 都期望相同的输入,我希望 mainFoo 将发送相同的参数。这带来了3个问题

1) 在堆栈上加载结构与在该堆栈上加载结构的地址到底意味着什么?我的意思是,大小大于 8 字节(64 位)的结构不能"坐在"堆栈上。

2) CLR 是否在生成结构的副本之前只是用作"this"(根据 C# 规范,我们知道这是真的)?此副本存储在哪里?fooMain 程序集显示调用方法在其堆栈上生成副本。

3)似乎通过值和地址加载结构(ldarg/ldloc vs ldarga/ldloca)实际上加载了一个地址 - 对于第二组,它只是在之前创建一个副本。为什么?我在这里错过了什么吗?

4) 回到迭代器/异步 - foo/foo1 示例是否复制了迭代器和非迭代器结构体的"this"参数之间的差异?为什么需要这种行为?创建副本似乎是浪费工作。动机是什么?

(此示例是使用 .Net Framework4.5 拍摄的,但使用 .Net Framework 2 和 CoreCLR 也可以看到相同的行为)

我将引用 ECMA 335 规范,该规范定义了 C# 所基于的 CLR,然后我们将看到它如何回答您的问题。


I.8.9.7 值类型定义
snip

  1. 当在值类型上调用非静态方法(即实例或虚拟方法)时,其 this 指针是对实例的托管引用,其中,当在关联的盒装类型上调用该方法时,this 指针是对象引用。/<>值类型上的实例方法接收this指针是指向未装箱类型的托管指针,而虚拟方法(包括由值类型实现的接口上的方法)接收装箱类型的实例。

这告诉我们,结构的实例方法(如上面的foo1())有一个表示为托管引用的this指针,即指向实际结构的 GC 指针,您在 C# 中将其称为ref

对于已知属于该类型的装箱结构,可以在不取消装箱的情况下调用方法,CLR 将自动传递ref指针。见二.13.3。


现在,如果我们需要从存储在本地ref或直接加载到堆栈中的结构访问字段,会发生什么?

III.4.10 ldfld – 物体的载荷场

堆栈转换

obj=>...

ldfld 指令将 obj 字段的值推送到堆栈上。 obj 应为对象(O 类型)、托管指针(&)、一个 非托管指针(本机 int 类型)或值类型的实例。

因此,无论结构在哪里,我们都可以使用ldfld来获取值。堆栈上的整个值被弹出,并加载值。
foo()中,你在堆栈(ldloc.0)上按值传递结构,方法也做同样的事情(ldarg.0).
foo1()中,结构被 ref (ldloca.s) 作为this传递,并且它由 ref 加载(这里ldarg.0表示 ref)。


稍后将涉及以下内容。

I.8.2.1 托管指针和相关类型

剪......它们不能用于字段签名...
snip理由:出于性能原因,GC 堆上的项目可能不包含对其他 GC 对象内部的引用,这激发了对字段的限制...


现在回答您的问题:

  1. 我们可以将结构直接加载到堆栈中。无论结构有多少字节,这都会占用
  2. 您的示例不是迭代器或异步的情况。ECMA-334 12.7.8 的 c# 规范说这是一个引用,所以这实际上是一个可变指针。您可以通过改变foo1()中的结构来证明这一点。
  3. 当涉及到foo()中的 JITted 汇编程序时,您的结构示例有点例外。似乎 JIT 将针对大于 8 字节的结构进行优化,并在可能的情况下按 ref 传递它,即不更改语义。
  4. 在实际的异步或迭代器函数中,参数被转换为编译器生成的结构的字段,该结构用作状态机。CLR 不允许在字段中存储引用,因此必须遵循按值语义。

相关内容

  • 没有找到相关文章