net - Por qué el compilador c#en algunos casos emite newobj/stobj en lugar de ''instancia de llamada.ctor'' para la inicialización de la estructura
código il (2)
Aquí un programa de prueba en c #:
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);
}
}
y aquí desmontamos la versión compilada del método 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
No entiendo por qué se emitió newobj / stobj en lugar de simple llamada .ctor? Para hacerlo más misterioso, newobj + stobj optimizado por jit-compiler en modo de 32 bits para una llamada ctor, pero no en modo de 64 bits ...
ACTUALIZAR:
Para aclarar mi confusión, a continuación están mis expectativas.
expresión de declaración de tipo de valor como
Foo foo = new Foo(10)
debe ser compilado a través de
call instance void Foo::.ctor(int32)
expresión de declaración de tipo de valor como
Foo foo = default(Foo)
debe ser compilado a través de
initobj Foo
en mi opinión, la variable temporal en el caso de la expresión de construcción, o la instancia de la expresión por defecto se debe considerar como variable objetivo, ya que esto no puede seguir a ningún comportamiento peligroso
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
}
expresión de asignación como
foo = new Foo(10); // foo declared somewhere before
debe ser compilado a algo como esto:
.locals init (
...
valuetype Foo __temp,
...
)
...
ldloca __temp
ldc.i4 10
call instance void Foo::.ctor(int32)
ldloc __temp
stloc foo
...
esta es la forma en que entiendo lo que dice la especificación de C #:
7.6.10.1 Expresiones de creación de objetos.
...
El procesamiento en tiempo de ejecución de una expresión de creación de objeto de la forma nueva T (A), donde T es de tipo de clase o de tipo struct y A es una lista de argumentos opcional, consta de los siguientes pasos:
...
Si T es un tipo de estructura:
Se crea una instancia de tipo T asignando una variable local temporal. Dado que se requiere un constructor de instancia de un tipo de estructura para asignar definitivamente un valor a cada campo de la instancia que se está creando, no es necesaria la inicialización de la variable temporal.
El constructor de instancia se invoca de acuerdo con las reglas de invocación de miembro de función (§7.5.4). Una referencia a la instancia recién asignada se pasa automáticamente al constructor de la instancia y se puede acceder a la instancia desde ese constructor como esto.
Quiero hacer énfasis en "asignar una variable local temporal". y en mi entendimiento, la instrucción newobj asume la creación del objeto en el montón ...
La dependencia de la creación de objetos de la forma en que se usó me derribó en este caso, ya que foo1 y foo2 tienen el mismo aspecto que yo.
En primer lugar, debes leer mi artículo sobre este tema. No aborda su escenario específico , pero tiene una buena información de fondo:
http://blogs.msdn.com/b/ericlippert/archive/2010/10/11/debunking-another-myth-about-value-types.aspx
Bien, ahora que has leído que sabes que la especificación de C # establece que la construcción de una instancia de una estructura tiene estas semánticas:
- Cree una variable temporal para almacenar el valor de la estructura, inicializado al valor predeterminado de la estructura.
- Pase una referencia a esa variable temporal como "esto" del constructor
Así que cuando dices:
Foo foo = new Foo(123);
Eso es equivalente a:
Foo foo;
Foo temp = default(Foo);
Foo.ctor(ref temp, 123); // "this" is a ref to a variable in a struct.
foo1 = temp;
Ahora, puede preguntar por qué pasar por todos los problemas de asignar un temporal cuando ya tenemos una variable foo
allí mismo que podría ser this
:
Foo foo = default(Foo);
Foo.ctor(ref foo, 123);
Esa optimización se llama copia elision . Al compilador de C # y / o la fluctuación de fase se les permite realizar una elision de copia cuando determinan, usando sus heurísticas, que hacerlo siempre es invisible . Hay raras circunstancias en las que una elección de copia puede causar un cambio observable en el programa, y en esos casos no se debe utilizar la optimización. Por ejemplo, supongamos que tenemos una estructura de pares de entradas:
Pair p = default(Pair);
try { p = new Pair(10, 20); } catch {}
Console.WriteLine(p.First);
Console.WriteLine(p.Second);
Esperamos que p
aquí (0, 0)
o (10, 20)
, nunca (10, 0)
o (0, 20)
, incluso si el ctor se lanza a la mitad. Es decir, o bien la asignación a p
fue del valor completamente construido, o no se realizó ninguna modificación en p
. El elision de copia no se puede realizar aquí; tenemos que hacer un temporal, pasar el temporal al ctor, y luego copiar el temporal a la p
.
Del mismo modo, supongamos que tenemos esta locura:
Pair p = default(Pair);
p = new Pair(10, 20, ref p);
Console.WriteLine(p.First);
Console.WriteLine(p.Second);
Si el compilador de C # realiza la elision de copia, entonces this
y ref p
son alias de p
, lo que es notablemente diferente a si es un alias temporal. El ctor podría observar que los cambios en this
causa cambian a ref p
si alias la misma variable, pero no lo observarían si alias diferentes variables.
La heurística del compilador de C # está decidiendo a hacer la elección de copia en foo1
pero no foo2
en su programa. Es ver que hay un ref foo2
en tu método y decidir allí mismo rendirte. Podría hacer un análisis más sofisticado para determinar que no está en una de estas locas situaciones de alias, pero no lo hace. Lo barato y fácil de hacer es saltarse la optimización si existe alguna posibilidad, por remota que sea, de que pueda haber una situación de alias que haga visible la elección. Genera el código newobj
y deja que el jitter decida si quiere hacer la elección.
En cuanto al jitter: los jitter de 64 y 32 bits tienen optimizadores completamente diferentes. Al parecer, uno de ellos está decidiendo que puede introducir la elección de copia que el compilador de C # no lo hizo, y el otro no.
Eso es porque las variables foo1
y foo2
son diferentes.
La variable foo1
es solo un valor, pero la variable foo2
es tanto un valor como un puntero cuando se usa en una llamada con la palabra clave ref
.
Cuando se inicializa la variable foo2
, el puntero se configura para que apunte al valor, y se llama al constructor con el valor del puntero en lugar de la dirección del valor.
Si configura dos métodos PrintFoo
con la única diferencia de que uno tiene la palabra clave ref
, y los llama con una variable cada uno:
Foo a = new Foo(10);
Foo b = new Foo(20);
PrintFoo(ref a);
PrintFoo(b);
Si descompila el código generado, la diferencia entre las variables es visible:
&Foo a = new Foo(10);
Foo b = new Foo(20);
Program.PrintFoo(ref a);
Program.PrintFoo(b);