我目前正在使用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版: www.ecma-international/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: www.ecma-international/publications/files/ECMA-ST/ECMA-334.pdf) states (12.7.8 This access):
"...如果方法或访问器是迭代器或异步函数,则此变量表示以下内容的副本:为其调用方法或访问器的结构,...."
"... 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"是通过值发送的,而不是通过引用发送的.我确实看到副本没有在外面修改过.我正试图了解结构实际上是如何发送的.
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.
I.8.9.7值类型定义 snip
这告诉我们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?
III.4.10 ldfld –对象的加载字段
堆栈过渡
... 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 &), an unmanaged 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).
以下内容将很快适用.
I.8.2.1托管指针和相关类型
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:
更多推荐
了解“此"结构的参数(特别是Iterators/async)
发布评论