Почему компилятор С# в некоторых случаях испускает newobj/stobj, а не "call instance.ctor" для инициализации структуры

здесь некоторая тестовая программа в С#:

using System;


struct Foo {
    int x;
    public Foo(int x) {
        this.x = x;
    }
    public override string ToString() {
        return x.ToString();
    }
}

class Program {
    static void PrintFoo(ref Foo foo) {
        Console.WriteLine(foo);
    }

    static void Main(string[] args) {
        Foo foo1 = new Foo(10);
        Foo foo2 = new Foo(20);

        Console.WriteLine(foo1);
        PrintFoo(ref foo2);
    }
}

и здесь дизассемблированная скомпилированная версия метода Main:

.method private hidebysig static void Main (string[] args) cil managed {
    // Method begins at RVA 0x2078
    // Code size 42 (0x2a)
    .maxstack 2
    .entrypoint
    .locals init (
        [0] valuetype Foo foo1,
        [1] valuetype Foo foo2
    )

    IL_0000: ldloca.s foo1
    IL_0002: ldc.i4.s 10
    IL_0004: call instance void Foo::.ctor(int32)
    IL_0009: ldloca.s foo2
    IL_000b: ldc.i4.s 20
    IL_000d: newobj instance void Foo::.ctor(int32)
    IL_0012: stobj Foo
    IL_0017: ldloc.0
    IL_0018: box Foo
    IL_001d: call void [mscorlib]System.Console::WriteLine(object)
    IL_0022: ldloca.s foo2
    IL_0024: call void Program::PrintFoo(valuetype Foo&)
    IL_0029: ret
} // end of method Program::Main

Я не понимаю, почему newobj/stobj был издан вместо простого вызова .ctor? Чтобы сделать его более загадочным, newobj + stobj оптимизирован jit-компилятором в 32-битном режиме на один вызов ctor, но он не находится в режиме 64 бит...

UPDATE:

Чтобы пояснить мое замешательство, ниже мое ожидание.

выражение типа значения типа типа

Foo foo = new Foo(10)

следует скомпилировать с помощью

call instance void Foo::.ctor(int32)

выражение типа значения типа типа

Foo foo = default(Foo)

следует скомпилировать с помощью

initobj Foo

на мой взгляд, временная переменная в случае конструктивного выражения или экземпляр выражения по умолчанию следует рассматривать как целевую переменную, так как это не может следовать за любым опасным поведением

try{
    //foo invisible here
    ...
    Foo foo = new Foo(10);
    //we never get here, if something goes wrong
}catch(...){
    //foo invisible here
}finally{
    //foo invisible here
}

выражение присваивания типа

foo = new Foo(10); // foo declared somewhere before

должен быть скомпилирован примерно так:

.locals init (
    ...
    valuetype Foo __temp,
    ...
)

...
ldloca __temp
ldc.i4 10
call instance void Foo::.ctor(int32)
ldloc __temp
stloc foo
...

так я понимаю, что говорит спецификация С#:

7.6.10.1 Выражения создания объектов

...

Обработка времени создания объекта-выражения формы new T (A), где T является классом или структурным типом, а A является необязательным списком аргументов, состоит из следующих шагов:

...

Если T является структурным типом:

  • Экземпляр типа T создается путем выделения временной переменной local. Поскольку конструктор экземпляра struct-type требуется для того, чтобы определенно присвоить значение каждому полю создаваемого экземпляра, не требуется инициализация временной переменной.

  • Конструктор экземпляра вызывается в соответствии с правилами вызова функции-члена (§7.5.4). Ссылка на вновь выделенный экземпляр автоматически передается в конструктор экземпляра, и экземпляр может быть доступен изнутри этого конструктора, как это.

Я хочу сделать упор на "выделение временной локальной переменной". и в моем понимании newobj инструкция предполагает создание объекта на куче...

Зависимость создания объекта от того, как он используется, заставляет меня в этом случае, так как foo1 и foo2 выглядят одинаково для меня.

Ответ 1

Прежде всего, вы должны прочитать мою статью по этому вопросу. Он не учитывает ваш конкретный сценарий, но он содержит некоторую хорошую справочную информацию:

http://blogs.msdn.com/b/ericlippert/archive/2010/10/11/debunking-another-myth-about-value-types.aspx

Итак, теперь, когда вы прочитали, что знаете, что спецификация С# заявляет, что при построении экземпляра структуры есть такая семантика:

  • Создайте временную переменную, чтобы сохранить значение struct, инициализированное значением по умолчанию для структуры.
  • Передайте ссылку на эту временную переменную как "this" конструктора

Итак, когда вы говорите:

Foo foo = new Foo(123);

Это эквивалентно:

Foo foo;
Foo temp = default(Foo);
Foo.ctor(ref temp, 123); // "this" is a ref to a variable in a struct.
foo1 = temp;

Теперь вы можете спросить, почему все проблемы выделения временного объекта, когда у нас уже есть переменная foo, которая может быть this:

Foo foo = default(Foo);
Foo.ctor(ref foo, 123);

Эта оптимизация называется копией. Компилятору С# и/или дрожанию разрешено выполнять эмиссию копии, когда они определяют использование своей эвристики, что делает это всегда невидимым. Существуют редкие обстоятельства, при которых копирование может вызвать заметное изменение в программе, и в таких случаях оптимизация не должна использоваться. Например, предположим, что у нас есть пара-ints struct:

Pair p = default(Pair);
try { p = new Pair(10, 20); } catch {}
Console.WriteLine(p.First);
Console.WriteLine(p.Second);

Мы ожидаем, что p здесь либо (0, 0), либо (10, 20), никогда (10, 0) или (0, 20), даже если ctor выбрасывается на полпути. То есть либо присвоение p было полностью сконструированным значением, либо вообще не было изменено на p. Экземпляр копирования не может быть выполнен здесь; мы должны сделать временное, передать временное значение в ctor, а затем скопировать временное значение в p.

Аналогично, предположим, что у нас было это безумие:

Pair p = default(Pair);
p = new Pair(10, 20, ref p);
Console.WriteLine(p.First);
Console.WriteLine(p.Second);

Если компилятор С# выполняет эллипс копирования, то this и ref p оба являются псевдонимами для p, что, очевидно, отличается от if this является псевдонимом временного! Ctor мог заметить, что изменения в this вызывают изменения в ref p, если они имеют одну и ту же переменную, но не будут замечать, что если они сглаживают разные переменные.

Эвристика компилятора С# решает сделать копию в foo1, но не foo2 в вашей программе. Он видит, что в вашем методе есть ref foo2 и решается прямо сейчас отказаться. Он мог бы сделать более сложный анализ, чтобы определить, что он не находится в одной из этих сумасшедших ситуаций сглаживания, но это не так. Дешевая и простая задача - просто пропустить оптимизацию, если есть какая-то вероятность, хотя и удаленная, что может быть ситуация сглаживания, которая делает видимость видимой. Он генерирует код newobj и позволяет дрожанию решить, хочет ли он сделать исключение.

Что касается джиттера: у 64-битных и 32-битных джиттеров есть совершенно разные оптимизаторы. По-видимому, один из них решает, что он может ввести копию, которую не компилировал компилятор С#, а другой - нет.

Ответ 2

Это потому, что переменные foo1 и foo2 отличаются.

Переменная foo1 - это просто значение, но переменная foo2 является как значением, так и указателем, используемым при вызове с ключевым словом ref.

Когда переменная foo2 инициализируется, указатель настроен так, чтобы указывать на значение, а конструктор вызывается со значением указателя, а не с адресом значения.

Если вы настроили два метода PrintFoo с той лишь разницей, что у вас есть ключевое слово ref, и вызовите их с одной переменной:

Foo a = new Foo(10);
Foo b = new Foo(20);
PrintFoo(ref a);
PrintFoo(b);

Если вы декомпилируете сгенерированный код, разница между переменными видна:

&Foo a = new Foo(10);
Foo b = new Foo(20);
Program.PrintFoo(ref a);
Program.PrintFoo(b);