Понимание аргумента "this" для структур (в частности, Iterators / async) - PullRequest
3 голосов
/ 10 июня 2019

В настоящее время я проверяю глубокие объекты в CLR с помощью Profiler API. У меня есть конкретная проблема, связанная с анализом аргумента «this» для итераторов / асинхронных методов (генерируемых компилятором в виде <name>d__123::MoveNext).

Исследуя это, я обнаружил, что действительно существует особое поведение. Во-первых, компилятор C # компилирует эти сгенерированные методы как структуры (только в режиме Release). ECMA-334 (спецификация языка C #, 5-е издание: https://www.ecma -international.org / публикации / файлы / 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 копию структуры, прежде чем просто использовать ее как «это» (мы знаем, что это правда, согласно спецификации C #)? Где хранится эта копия? Сборка fooMain показывает, что вызывающий метод генерирует копию в своем стеке.

3) Кажется, что загрузка структуры по значению и адресу (ldarg / ldloc vs ldarga / ldloca) фактически загружает адрес - для второго набора он просто создает копию ранее. Зачем? Я что-то здесь упускаю?

4) Возвращаясь к Iterators / async - повторяет ли пример foo / foo1 разницу между аргументом "this" для структур итераторов и не-итераторов? Почему это поведение требуется? Создание копии кажется пустой тратой времени. Какая мотивация?

(Этот пример взят с использованием .Net framework 4.5, но то же поведение наблюдается и с использованием .Net framework 2 и CoreCLR)

...