Понимание этого аргумента для структур (в частности, Iterators/async)

В настоящее время я проверяю глубокие объекты в CLR с помощью Profiler API. У меня есть конкретная проблема с анализом «этого» аргумента для итераторов/асинхронных методов (сгенерированных компилятором в форме <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 Этот доступ):

"... Если метод или метод доступа является итератором или асинхронной функцией, эта переменная представляет собой копию структуры, для которой был вызван метод или метод доступа,...."

Это означает, что в отличие от других аргументов «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 просто для того, чтобы мы могли правильно проверить JIT-код. Я смотрю на три разные вещи: как 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-код (и один и тот же JIT-код сборки). Это имеет смысл, поскольку в конечном итоге мы используем только первый аргумент. Второе, что мы видим, это то, что mainFoo вызывает два метода по-разному (ldloc и ldloca). Поскольку и foo, и foo1 ожидают одни и те же входные данные, я ожидаю, что mainFoo отправит одни и те же аргументы. Это вызвало 3 вопроса

1) Что именно означает загрузка структуры в стек по сравнению с загрузкой адреса структуры в этот стек? Я имею в виду, что структура размером более 8 байт (64 бита) не может «сидеть» в стеке.

2) Создает ли CLR копию структуры, прежде чем использовать ее как «эту» (мы знаем, что это правда, согласно спецификации С#)? Где хранится эта копия? Сборка fooMain показывает, что вызывающий метод создает копию в своем стеке.

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

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

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


person Egozy    schedule 10.06.2019    source источник
comment
Я имею в виду, что структура размером более 8 байт (64 бита) не может находиться в стеке. - почему нет? почти уверен, что может... он может не поместиться во многие регистры или предложить атомарность, но... стек - это просто пространство памяти...   -  person Marc Gravell    schedule 10.06.2019
comment
Я перефразирую - он, конечно, может сидеть в стеке (любая структура нормального размера). До сих пор, насколько я видел, стек оценки никогда не содержал ничего больше 8 байт. Например, если я генерирую IL-код для помещения в стек огромной структуры и извлечения только верхнего значения, стек вычислений очищается.   -  person Egozy    schedule 10.06.2019
comment
но огромная структура является значением, так что да, если вы вытащите только верхнее значение, я ожидаю, что вся структура исчезнет...?   -  person Marc Gravell    schedule 10.06.2019
comment
Звучит неплохо, просто я никогда не думал, что это так работает. Стек операндов JVM состоит только из 8-байтовых значений, поэтому у меня было ощущение, что это то же самое. Это вызывает много дополнительных вопросов (например, как CLR может узнать только из стека, что верхнее значение является структурой?). Я пока оставлю это в стороне. Спасибо. Мы все еще можем видеть, что обе команды load и load-address в конце концов помещают адрес стека, и это то, что интересно в данном случае.   -  person Egozy    schedule 10.06.2019
comment
это потому, что в JVM вам нужно думать только о ссылках (классах) и встроенных примитивах JVM, которые все оказываются небольшими; в .NET у нас есть пользовательские структуры произвольного размера; весь смысл структуры в том, что семантически это значение, поэтому один pop / ld является всей структурой   -  person Marc Gravell    schedule 10.06.2019
comment
load в этом случае обычно не помещает адрес в стек, но когда вы пытаетесь сделать что-то с этим значением в стеке, вам часто нужно использовать адрес стека в начале структуры ...   -  person Marc Gravell    schedule 10.06.2019


Ответы (1)


Я процитирую спецификацию ECMA 335. , который определяет среду CLR, на которой основан C#, а затем мы посмотрим, как это ответит на ваши вопросы.


I.8.9.7 Определение типа значения
snip

  1. Когда нестатический метод (т. е. экземпляр или виртуальный метод) вызывается для типа значения, его указатель this является управляемой ссылкой на экземпляр, тогда как когда метод вызывается для связанного упакованный тип, указатель this является ссылкой на объект.
    Методы экземпляра для типов значений получают указатель this, который является управляемым указателем на неупакованный тип, тогда как виртуальные методы (включая интерфейсы, реализованные типом значения) получают экземпляр упакованного типа.

Это говорит нам о том, что метод экземпляра структуры, такой как foo1() выше, имеет указатель this, который представлен как управляемая ссылка, то есть указатель GC на фактическую структуру, вы знаете это в C# как ссылка.

В случае упакованных структур, о которых известно, что они относятся к этому типу, можно вызвать метод без распаковки, CLR автоматически передаст указатель ref. См. II.13.3.


Что произойдет, если нам потребуется получить доступ к полю из структуры, хранящейся в local, ref или загруженной непосредственно в стек?

III.4.10 ldfld — поле загрузки объекта

Переход стека

... obj =› значение ...

Инструкция ldfld помещает в стек значение поля obj. obj должен быть объектом (тип O), управляемым указателем (тип &), неуправляемым указателем (тип native int) или экземпляром типа значения.

Таким образом, независимо от того, где находится структура, мы можем использовать ldfld для получения значения. Все значение в стеке извлекается, и значение загружается. Но вы должны понимать, что объект в логическом (теоретическом) стеке в каждом случае разный.
В foo() вы передаете структуру по значению в стеке (ldloc.0), и метод делает то же самое (ldarg.0).< br /> В foo1() структура передается как this по ссылке (ldloca.s) и загружается по ссылке (здесь ldarg.0 представляет ссылку).


Следующее будет актуальным в данный момент.

I.8.2.1 Управляемые указатели и связанные типы

snip ...их нельзя использовать для сигнатур полей...
snip Обоснование: из соображений производительности элементы в куче GC не могут содержать ссылки на внутреннюю часть других объектов GC, это мотивирует ограничения на поля...


Теперь, чтобы ответить на ваши вопросы:

  1. Мы можем загрузить структуру прямо в стек. Это займет сколько бы байтов ни была структура.
  2. Ваш пример не относится к итераторам или асинхронности. Спецификация c# в ECMA-334 12.7.8 говорит, что это ref, поэтому на самом деле это изменяемый указатель. Вы можете доказать это, изменив структуру в foo1().
  3. Ваш пример структуры является исключением, когда дело доходит до JITted-ассемблера в foo(). Кажется, что JIT оптимизирует структуру размером более 8 байт и передает ее по ссылке, где это возможно, то есть без изменения семантики.
  4. В настоящей асинхронной или итераторной функции параметры преобразуются в поля структуры, созданной компилятором, которая работает как конечный автомат. CLR не позволит сохранить ref в поле, поэтому необходимо соблюдать семантику по значению.
person Charlieface    schedule 25.12.2020