本文介绍了了解“此"结构的参数(特别是Iterators/async)的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I'm currently inspecting deep objects in the CLR using the Profiler API. I have a specific problem analyzing "this" argument for Iterators/async methods (generated by the compiler, in the form of <name>d__123::MoveNext).

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

While researching this I found that there is indeed a special behavior. First, the C# compiler compiles these generated methods as structs (only in Release mode). ECMA-334 (C# Language Specification, 5th edition: https://www.ecma-international.org/publications/files/ECMA-ST/ECMA-334.pdf) states (12.7.8 This access):

这意味着与其他"this"参数不同,在这种情况下,"this"是通过值发送的,而不是通过引用发送的.我确实看到副本没有在外面修改过.我正试图了解结构实际上是如何发送的.

This means that unlike other "this" arguments, in this case the "this" is send by value, not by reference. I indeed see the copy isn't modified outside. I'm trying to understand how, exactly, is the struct actually sent.

我自由地剥离了复杂的案子,并用一个小结构复制了这个案子.看下面的代码:

I took the liberty to strip down the complicated case, and replicate this with a small struct. Look at the following code:

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):

NoInlining is just so we can inspect the JITted code properly. I'm looking at three different things: how mainFoo calls foo/foo1, how foo is compiled and how foo1 is compiled.The following is the IL code generated (using 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

生成的汇编代码(仅相关零件):

The assembly code generated (relevant parts only):

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个问题

The first thing we can all see is that both foo and foo1 generates the same IL code (and the same JITted assembly code). This makes sense, since eventually we're just using the first argument. The second thing we see, is that mainFoo calls the two methods differently (ldloc vs ldloca). Since both foo and foo1 expects the same input, I would expect that mainFoo will send the same arguments. This brought up 3 questions

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

1) What exactly does it mean to load a struct on the stack vs loading a struct's address on that stack? I mean, a struct of size bigger than 8 bytes (64 bit), can't "sit" on the stack.

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

2) Is the CLR generating a copy of the struct before just to use as "this" (We know this is true, according to C# specification)? Where is this copy stored? fooMain assembly shows that the calling method generates the copy on it's stack.

3)似乎同时按值和地址(ldarg/ldloc与ldarga/ldloca)加载结构实际上都加载了地址-对于第二组,它之前仅创建了一个副本.为什么?我在这里想念什么吗?

3) It seems as though both loading a struct by value and address (ldarg/ldloc vs ldarga/ldloca) actually loads an address - for the second set it just creates a copy before. Why? Am I missing something here?

4)返回Iterators/async-foo/foo1示例是否在Iterators和amp; non-iterators结构的"this"自变量之间复制差异?为什么要这种行为?创建副本似乎是对工作的浪费.动机是什么?

4) Back to Iterators/async - is the foo/foo1 example replicating the difference between "this" argument for iterators&non-iterators structs? Why is this behavior wanted? Creating a copy seems like a waste of work. What's the motivation?

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

(This example is taken using .Net framework 4.5, but the same behavior is also seen using .Net framework 2 and CoreCLR)

推荐答案

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

I will quote from the ECMA 335 spec, which defines the CLR on which C# is based, and then we will see how that answers your questions.

  1. 在值类型上调用非静态方法(即实例或虚拟方法)时,其 this 指针是对该实例的托管引用,就像调用该方法时一样在相关的盒装类型上, this 指针是一个对象引用.
    值类型的实例方法会收到 this 指针,该指针是指向未装箱类型的托管指针,而虚拟方法(包括那些由值类型实现的接口上的方法)将接收到该装箱类型的实例.
  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())具有 this 指针,该指针表示为托管引用,即GC指针对于实际的结构,您在C#中以 ref 知道这一点.

This tells us that an instance method of struct, such as foo1() above, have a this pointer which is represented as a managed reference, i.e. a GC pointer to the actual struct, you know this in C# as a ref.

对于已知具有这种类型的装箱结构,可以在不取消装箱的情况下调用方法,CLR将自动传递 ref 指针.参见II.1.3.3.

In the case of boxed structs that are known to be of that type, it is possible to call a method without unboxing, the CLR will pass the ref pointer automatically. See II.13.3.

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

Now, what happens if we need to access the field from a struct stored in a local, a ref or loaded directly on the stack?

堆栈过渡

... obj => ...

... obj => value ...

ldfld指令将obj字段的值压入堆栈.obj应该是一个对象(类型O),一个托管指针(类型&),一个非托管指针(本机int类型)或值类型的实例.

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.

因此,无论结构在哪里,我们都可以使用 ldfld 来获取值.弹出堆栈上的整个值,并加载该值.但是您必须了解,逻辑(理论)堆栈上的对象在每种情况下都是不同的.
foo()中,您按栈中的值( ldloc.0 )传递结构,该方法执行相同的操作( ldarg.0 ).
foo1()中,该结构通过ref( ldloca.s )作为 this 传递,并由by-ref(此处为 ldarg.0 代表参考).

So no matter where the struct is, we can use ldfld to get the value. The entire value on the stack is popped, and the value loaded. But you must understand that the object on the logical (theoretical) stack is different in each case.
In foo(), you pass the struct by value on the stack (ldloc.0) and the method does the same (ldarg.0).
In foo1(), the struct is passed as this by ref (ldloca.s), and it's loaded by-ref (here ldarg.0 represents the ref).

以下内容将很快适用.

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

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...


现在回答您的问题:


Now to answer your questions:

  1. 我们可以将结构直接加载到堆栈中.无论结构是多少字节,这都将占用.
  2. 您的示例不是迭代器或异步的情况.ECMA-334 12.7.8上的c#规范说这是一个 ref ,因此它实际上是一个可变的指针.您可以通过更改 foo1()中的结构来证明这一点.
  3. 当涉及到 foo()中的JITted汇编程序时,您的结构示例有些例外.似乎JIT会针对大于8个字节的结构进行优化,并在可能的情况下(即不更改语义的情况)将其传递给by-ref.
  4. 在实际的异步或迭代器函数中,参数被转换为编译器生成的结构的字段,该结构用作状态机.CLR不允许在字段中存储 ref ,因此必须遵循按值语义.
  1. We can load a struct direct to the stack. This will take up however many bytes the struct is.
  2. Your example is not a case of iterators or async. The c# spec at ECMA-334 12.7.8 says this is a ref, so this is actually a mutable pointer. You can prove this by mutating the struct in foo1().
  3. Your example of a struct is a bit of an exception when it comes to the JITted assembler in foo(). It seems the JIT will optimize for a struct being bigger than 8 bytes and pass it by-ref where possible i.e. without changing the semantics.
  4. In an actual async or iterator function, the parameters are transformed into fields of a compiler-generated struct, which works as a state machine. The CLR will not permit a ref to be stored in a field, so by-value semantics must be followed.

这篇关于了解“此"结构的参数(特别是Iterators/async)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

07-24 14:25