gpt4 book ai didi

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

转载 作者:行者123 更新时间:2023-12-03 17:29:56 25 4
gpt4 key购买 nike

我目前正在使用 Profiler API 检查 CLR 中的深层对象。我在分析迭代器/异步方法的“this”参数(由编译器生成,形式为 <name>d__123::MoveNext )时遇到了一个具体问题。

在研究这个时,我发现确实有一种特殊的行为。首先,C# 编译器将这些生成的方法编译为结构体(仅在 Release 模式下)。 ECMA-334(C# 语言规范,第 5 版:https://www.ecma-international.org/publications/files/ECMA-ST/ECMA-334.pdf)指出(12.7.8 此访问):

"... If the method or accessor is an iterator or async function, the this variable represents a copy of the struct for which the method or accessor was invoked, ...."



这意味着与其他“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 与 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 也可以看到相同的行为)

最佳答案

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

I.8.9.7 Value type definition
snip

  1. When a non-static method (i.e., an instance or virtual method) is called on the value type, its this pointer is a managed reference to the instance, where as when the method is called on the associated boxed type, the this pointer is an object reference.
    Instance methods on value types receive a this pointer that is a managed pointer to the unboxed type whereas virtual methods (including those on interfaces implemented by the value type) receive an instance of the boxed type.

这告诉我们struct的一个实例方法,比如 foo1()上面,有一个 表示为托管引用的指针,即指向实际结构的 GC 指针,您在 C# 中将其称为 引用 .
对于已知属于该类型的装箱结构,可以在不拆箱的情况下调用方法,CLR 将传递 引用 指针自动。见 II.13.3。

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

III.4.10 ldfld – load field of an object

Stack Transition

... obj => value ...

The ldfld instruction pushes onto the stack the value of a field of obj.obj shall be an object (type O), a managed pointer (type &), anunmanaged pointer (type native int), or an instance of a value type.


所以无论struct在哪里,我们都可以使用 ldfld 以获得值(value)。堆栈上的整个值被弹出,并加载该值。但是您必须了解逻辑(理论)堆栈上的对象在每种情况下都是不同的。
foo() ,您在堆栈上按值传递结构( ldloc.0 )并且该方法执行相同的操作( ldarg.0 )。
foo1() , 结构体作为 this 传递通过 ref ( ldloca.s ),它是通过引用加载的(这里 ldarg.0 代表引用)。

稍后将涉及以下内容。

I.8.2.1 Managed pointers and related types

snip ...they cannot be used for field signatures...
snip Rationale: For performance reasons items on the GC heap may not contain references to the interior of other GC objects, this motivates the restrictions on fields...



现在回答您的问题:
  • 我们可以将结构直接加载到堆栈中。这将占用结构体的多少字节。
  • 您的示例不是迭代器或异步的情况。 ECMA-334 12.7.8 中的 c# 规范说这是一个 引用 ,所以这实际上是一个可变指针。你可以通过改变 foo1() 中的结构来证明这一点。 .
  • 当涉及到 foo() 中的 JITted 汇编程序时,您的结构示例有点异常(exception)。 .似乎 JIT 将针对大于 8 个字节的结构进行优化,并在可能的情况下通过引用传递它,即不改变语义。
  • 在实际的异步或迭代器函数中,参数被转换为编译器生成的结构的字段,该结构用作状态机。 CLR 将不允许 引用 要存储在字段中,因此必须遵循按值语义。
  • 关于c# - 了解结构的 "this"参数(特别是迭代器/异步),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56526095/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com