¿doble? = doble? + doble?

79

Quería hacer ping a la comunidad de StackOverflow para ver si estoy perdiendo la cabeza o no con este simple código C #.

Estoy desarrollando en Windows 7, construyendo esto en .NET 4.0, depuración x64.

Tengo el siguiente código:

static void Main()
{
    double? y = 1D;
    double? z = 2D;

    double? x;
    x = y + z;
}

Si depuro y pongo un punto de interrupción en la llave final, espero que x = 3 en la Ventana de visualización y la Ventana Inmediato. x = nulo en su lugar.

Si depuro en x86, las cosas parecen funcionar bien. ¿Hay algún problema con el compilador x64 o hay algún problema conmigo?

MrE
fuente
15
Esto es relevante para mis intereses. Ahora solo tenemos que esperar al Sr. Skeet.
Mike G
3
¿Podría ser el depurador? Intente poner Console.WriteLine () al final y vea lo que imprime.
siride
3
Esto parece una ejecución diferida que el depurador no puede ver más allá. Es posible que la asignación no se realice hasta que sea necesario.
Joel Etherton
5
@igrimpe ¿Aparentemente 1 + 2 = 25 para valores muy grandes de 1 y 2?
Chris Sinclair
2
Gracias por la respuesta de todos. Es genial ver a la comunidad saltar sobre esto tan rápido. El comportamiento del compilador x64 hace que la depuración de este tipo de declaraciones sea un poco poco intuitiva en mi opinión, pero al menos parece que la ejecución del código funciona correctamente en el gran esquema de la propia aplicación.
MrE

Respuestas:

86

La respuesta de Douglas es correcta sobre el código muerto de optimización de JIT ( tanto los compiladores x86 como los x64 harán esto). Sin embargo, si el compilador JIT estuviera optimizando el código muerto, sería inmediatamente obvio porquex ni siquiera aparecería en la ventana Locales. Además, el reloj y la ventana inmediata le darían un error al intentar acceder: "El nombre 'x' no existe en el contexto actual". Eso no es lo que ha descrito que está sucediendo.

Lo que está viendo es en realidad un error en Visual Studio 2010.

Primero, intenté reproducir este problema en mi máquina principal: Win7x64 y VS2012. Para los objetivos .NET 4.0, xes igual a 3.0D cuando se rompe en la llave de cierre. Decidí probar también los destinos .NET 3.5, y con eso,x también se configuró en 3.0D, no nulo.

Como no puedo hacer una reproducción perfecta de este problema ya que tengo .NET 4.5 instalado sobre .NET 4.0, puse en marcha una máquina virtual e instalé VS2010 en ella.

Aquí, pude reproducir el problema. Con un punto de interrupción en el corchete de cierre del Mainmétodo, tanto en la ventana del reloj como en la ventana de locales, vi quex era null. Aquí es donde comienza a ponerse interesante. En su lugar, apunté al tiempo de ejecución v2.0 y descubrí que allí también era nulo. Seguramente ese no puede ser el caso ya que tengo la misma versión del tiempo de ejecución de .NET 2.0 en mi otra computadora que se mostró exitosamente xcon un valor de 3.0D.

Entonces, ¿qué está pasando entonces? Después de investigar un poco en windbg, encontré el problema:

VS2010 le muestra el valor de x antes de que se le haya asignado .

Sé que no es así, ya que el puntero de instrucción está más allá de la x = y + zlínea. Puede probar esto usted mismo agregando algunas líneas de código al método:

double? y = 1D;
double? z = 2D;

double? x;
x = y + z;

Console.WriteLine(); // Don't reference x here, still leave it as dead code

Con un punto de interrupción en la llave final, los lugareños y la ventana de observación se muestran xcomo iguales a 3.0D. Sin embargo, si paso a través del código, usted notará que VS2010 no muestra xcomo asignados hasta después de que hubiera entrado a través de la Console.WriteLine().

No sé si este error se informó alguna vez a Microsoft Connect, pero es posible que desee hacerlo, con este código como ejemplo. Sin embargo, claramente se ha solucionado en VS2012, por lo que no estoy seguro de si habrá una actualización para solucionarlo o no.


Esto es lo que está sucediendo realmente en el JIT y el VS2010

Con el código original, podemos ver qué está haciendo VS y por qué está mal. También podemos ver que elx variable no se está optimizando (a menos que haya marcado el ensamblado para que se compile con las optimizaciones habilitadas).

Primero, veamos las definiciones de variables locales del IL:

.locals init (
    [0] valuetype [mscorlib]System.Nullable`1<float64> y,
    [1] valuetype [mscorlib]System.Nullable`1<float64> z,
    [2] valuetype [mscorlib]System.Nullable`1<float64> x,
    [3] valuetype [mscorlib]System.Nullable`1<float64> CS$0$0000,
    [4] valuetype [mscorlib]System.Nullable`1<float64> CS$0$0001,
    [5] valuetype [mscorlib]System.Nullable`1<float64> CS$0$0002)

Esta es una salida normal en modo de depuración. Visual Studio define variables locales duplicadas que se utilizan durante las asignaciones y luego agrega comandos IL adicionales para copiarlo de la variable CS * a su respectiva variable local definida por el usuario. Aquí está el código IL correspondiente que muestra que esto sucede:

// For the line x = y + z
L_0045: ldloca.s CS$0$0000 // earlier, y was stloc.3 (CS$0$0000)
L_0047: call instance !0 [mscorlib]System.Nullable`1<float64>::GetValueOrDefault()
L_004c: conv.r8            // Convert to a double
L_004d: ldloca.s CS$0$0001 // earlier, z was stloc.s CS$0$0001
L_004f: call instance !0 [mscorlib]System.Nullable`1<float64>::GetValueOrDefault()
L_0054: conv.r8            // Convert to a double 
L_0055: add                // Add them together
L_0056: newobj instance void [mscorlib]System.Nullable`1<float64>::.ctor(!0) // Create a new nulable
L_005b: nop                // NOPs are placed in for debugging purposes
L_005c: stloc.2            // Save the newly created nullable into `x`
L_005d: ret 

Hagamos una depuración más profunda con WinDbg:

Si depura la aplicación en VS2010 y deja un punto de interrupción al final del método, podemos adjuntar WinDbg fácilmente, en modo no invasivo.

Aquí está el marco para el Main método en la pila de llamadas. Nos preocupamos por la IP (puntero de instrucción).

0: 009>! Clrstack
ID del hilo del SO: 0x135c (9)
Sitio de llamada IP de SP secundario
000000001c48dc00 000007ff0017338d ConsoleApplication1.Program.Main (System.String [])
[Y así...]

Si vemos el código de máquina nativo del Mainmétodo, podemos ver qué instrucciones se han ejecutado en el momento en que VS interrumpe la ejecución:

000007ff`00173388 e813fe25f2 llamar mscorlib_ni + 0xd431a0 
           (000007fe`f23d31a0) (System.Nullable`1 [[System.Double, mscorlib]] .. ctor (Double), mdToken: 0000000006001ef2)
**** 000007ff`0017338d cc int 3 ****
000007ff`0017338e 8d8c2490000000 lea ecx, [rsp + 90h]
000007ff`00173395 488b01 mov rax, qword ptr [rcx]
000007ff`00173398 4889842480000000 mov qword ptr [rsp + 80h], rax
000007ff`001733a0 488b4108 mov rax, qword ptr [rcx + 8]
000007ff`001733a4 4889842488000000 mov qword ptr [rsp + 88h], rax
000007ff`001733ac 488d8c2480000000 lea rcx, [rsp + 80h]
000007ff`001733b4 488b01 mov rax, qword ptr [rcx]
000007ff`001733b7 4889442440 mov qword ptr [rsp + 40h], rax
000007ff`001733bc 488b4108 mov rax, qword ptr [rcx + 8]
000007ff`001733c0 4889442448 mov qword ptr [rsp + 48h], rax
000007ff`001733c5 eb00 jmp 000007ff`001733c7
000007ff`001733c7 0f28b424c0000000 movaps xmm6, xmmword ptr [rsp + 0C0h]
000007ff`001733cf 4881c4d8000000 agregar rsp, 0D8h
000007ff`001733d6 c3 ret

El uso de la IP actual que nos dieron de !clrstacken Main, vemos que la ejecución fue suspendida en la instrucción inmediatamente después de la llamada al System.Nullable<double>constructor 's. ( int 3es la interrupción utilizada por los depuradores para detener la ejecución) He rodeado esa línea con *, y también puede hacer coincidir la línea L_0056en el IL.

El ensamblado x64 que sigue en realidad lo asigna a la variable local x. Nuestro puntero de instrucción aún no ha ejecutado ese código, por lo que VS2010 se rompe prematuramente antes de que el xcódigo nativo asigne la variable.

EDITAR: En x64, la int 3instrucción se coloca antes del código de asignación, como puede ver arriba. En x86, esa instrucción se coloca después del código de asignación. Eso explica por qué VS se está rompiendo temprano solo en x64. Es difícil decir si esto es culpa de Visual Studio o del compilador JIT. No estoy seguro de qué aplicación inserta ganchos de puntos de interrupción.

Christopher Currens
fuente
6
@ChristopherCurrens Muy bien, te ganaste la marca de verificación verde. Esto tiene sentido. Enviaré algo a Microsoft. Gracias por la atención de todos a esto. Muy impresionado por el esfuerzo de la comunidad.
MrE
30

Se sabe que el compilador x64 JIT es más agresivo en sus optimizaciones que el x86. (Puede consultar " Eliminación de comprobación de límites de matriz en CLR " para un caso en el que los compiladores x86 y x64 generan código que es semánticamente diferente).

En este caso, el compilador x64 está detectando que xnunca se lee y elimina por completo su asignación; esto se conoce como eliminación de código muerto en la optimización del compilador. Para evitar que esto suceda, simplemente agregue la siguiente línea justo después de la tarea:

Console.WriteLine(x);

Observará que no solo 3se imprime el valor correcto de , sino que la variable xtambién mostraría el valor correcto en el depurador (editar) después de la Console.WriteLinellamada que lo hace referencia.

Editar : Christopher Currens ofrece una explicación alternativa que apunta a un error en Visual Studio 2010, que podría ser más precisa que la anterior.

Douglas
fuente
Er, acabo de probar esto ya que este también fue mi primer pensamiento, y esta respuesta es realmente incorrecta. El depurador realmente muestra nulo después de la asignación hasta después de la llamada a Console.WriteLine().
Tomfanning
@tomfanning: Eso no invalida la respuesta. El compilador emite la instrucción para realizar la asignación solo en el punto en el que detecta que será necesaria, en el Console.WriteLine.
Douglas
1
@leppie El compilador es (o al menos solía ser) diferente. Por ejemplo, se integrará de forma mucho más agresiva que el compilador x86.
Konrad Rudolph
2
@leppie: “ Array Bounds Check Elimination en CLR ” da ejemplos de dónde los compiladores pueden dar lugar a diferencias semánticas. “Los compiladores JIT para x86 y x64 son actualmente bases de código bastante diferentes […] El x86 JIT es más rápido en términos de velocidad de compilación; el x64 JIT es más lento, pero realiza optimizaciones más interesantes ".
Douglas
2
@MrE: esto no debe marcarse como la respuesta correcta. Si el compilador JIT estuviera optimizando el código muerto, Visual Studio no podría resolver la xvariable en absoluto. Que no aparecería en la ventana de la población local, y tratando de visualizarla en el reloj o ventanas inmediatos daría el mensaje: The name 'x' does not exist in the current context.
Christopher Currens