¿Error potencial de .NET JIT?

404

El siguiente código proporciona un resultado diferente cuando se ejecuta la versión dentro de Visual Studio y se ejecuta fuera de Visual Studio. Estoy usando Visual Studio 2008 y apunto a .NET 3.5. También probé .NET 3.5 SP1.

Cuando se ejecuta fuera de Visual Studio, el JIT debería funcionar. O (a) está sucediendo algo sutil con C # que me falta o (b) el JIT está realmente en error. Dudo que el JIT pueda salir mal, pero me estoy quedando sin otras posibilidades ...

Salida cuando se ejecuta dentro de Visual Studio:

    0 0,
    0 1,
    1 0,
    1 1,

Salida al ejecutar la versión fuera de Visual Studio:

    0 2,
    0 2,
    1 2,
    1 2,

¿Cuál es la razón?

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Test
{
    struct IntVec
    {
        public int x;
        public int y;
    }

    interface IDoSomething
    {
        void Do(IntVec o);
    }

    class DoSomething : IDoSomething
    {
        public void Do(IntVec o)
        {
            Console.WriteLine(o.x.ToString() + " " + o.y.ToString()+",");
        }
    }

    class Program
    {
        static void Test(IDoSomething oDoesSomething)
        {
            IntVec oVec = new IntVec();
            for (oVec.x = 0; oVec.x < 2; oVec.x++)
            {
                for (oVec.y = 0; oVec.y < 2; oVec.y++)
                {
                    oDoesSomething.Do(oVec);
                }
            }
        }

        static void Main(string[] args)
        {
            Test(new DoSomething());
            Console.ReadLine();
        }
    }
}
Philip Welch
fuente
8
Sí, ¿qué tal eso? Encontrar un error grave en algo tan esencial como el .Net JIT: ¡felicidades!
Andras Zoltan
73
Esto parece reprobar en mi versión del 9 de diciembre del framework 4.0 en x86. Lo pasaré al equipo nervioso. ¡Gracias!
Eric Lippert el
28
Esta es una de las pocas preguntas que realmente merecen una insignia de oro.
Mehrdad Afshari
28
El hecho de que todos estamos interesados ​​en esta pregunta muestra que no esperamos errores en el .NET JIT, bien hecho Microsoft.
Ian Ringrose
2
Todos estamos esperando que Microsoft responda con ansiedad .....
Talha

Respuestas:

211

Es un error del optimizador JIT. Desenrolla el bucle interno pero no actualiza el valor oVec.y correctamente:

      for (oVec.x = 0; oVec.x < 2; oVec.x++) {
0000000a  xor         esi,esi                         ; oVec.x = 0
        for (oVec.y = 0; oVec.y < 2; oVec.y++) {
0000000c  mov         edi,2                           ; oVec.y = 2, WRONG!
          oDoesSomething.Do(oVec);
00000011  push        edi  
00000012  push        esi  
00000013  mov         ecx,ebx 
00000015  call        dword ptr ds:[00170210h]        ; first unrolled call
0000001b  push        edi                             ; WRONG! does not increment oVec.y
0000001c  push        esi  
0000001d  mov         ecx,ebx 
0000001f  call        dword ptr ds:[00170210h]        ; second unrolled call
      for (oVec.x = 0; oVec.x < 2; oVec.x++) {
00000025  inc         esi  
00000026  cmp         esi,2 
00000029  jl          0000000C 

El error desaparece cuando dejas que oVec.y se incremente a 4, son demasiadas llamadas para desenrollar.

Una solución es esta:

  for (int x = 0; x < 2; x++) {
    for (int y = 0; y < 2; y++) {
      oDoesSomething.Do(new IntVec(x, y));
    }
  }

ACTUALIZACIÓN: revisado en agosto de 2012, este error se corrigió en la versión 4.0.30319 jitter. Pero aún está presente en la v2.0.50727 jitter. Parece poco probable que solucionen esto en la versión anterior después de tanto tiempo.

Hans Passant
fuente
3
+1, definitivamente un error: podría haber identificado las condiciones para el error (¡sin decir que nobugz lo encontró por mí, sin embargo!), Pero esto (y el tuyo, Nick, así que +1 para ti también) muestra que el JIT Es el culpable. Es interesante que la optimización se elimine o sea diferente cuando IntVec se declare como una clase. Incluso si inicializa explícitamente los campos de estructura a 0 antes del bucle, se ve el mismo comportamiento. ¡Asqueroso!
Andras Zoltan
3
@Hans Passant ¿Qué herramienta usaste para generar el código de ensamblaje?
3
@Joan: solo Visual Studio, copie / pegue desde la ventana Desmontaje del depurador y agregue los comentarios a mano.
Hans Passant
82

Creo que esto está en un error de compilación JIT genuino. Lo informaría a Microsoft y vería lo que dicen. Curiosamente, descubrí que el x64 JIT no tiene el mismo problema.

Aquí está mi lectura del x86 JIT.

// save context
00000000  push        ebp  
00000001  mov         ebp,esp 
00000003  push        edi  
00000004  push        esi  
00000005  push        ebx  

// put oDoesSomething pointer in ebx
00000006  mov         ebx,ecx 

// zero out edi, this will store oVec.y
00000008  xor         edi,edi 

// zero out esi, this will store oVec.x
0000000a  xor         esi,esi 

// NOTE: the inner loop is unrolled here.
// set oVec.y to 2
0000000c  mov         edi,2 

// call oDoesSomething.Do(oVec) -- y is always 2!?!
00000011  push        edi  
00000012  push        esi  
00000013  mov         ecx,ebx 
00000015  call        dword ptr ds:[002F0010h] 

// call oDoesSomething.Do(oVec) -- y is always 2?!?!
0000001b  push        edi  
0000001c  push        esi  
0000001d  mov         ecx,ebx 
0000001f  call        dword ptr ds:[002F0010h] 

// increment oVec.x
00000025  inc         esi  

// loop back to 0000000C if oVec.x < 2
00000026  cmp         esi,2 
00000029  jl          0000000C 

// restore context and return
0000002b  pop         ebx  
0000002c  pop         esi  
0000002d  pop         edi  
0000002e  pop         ebp  
0000002f  ret     

Esto parece una optimización que me salió mal ...

Nick Guerrera
fuente
23

Copié tu código en una nueva aplicación de consola.

  • Build de depuración
    • Salida correcta con depurador y sin depurador
  • Cambiado a Release Build
    • De nuevo, salida correcta las dos veces
  • Creé una nueva configuración x86 (estoy ejecutando X64 Windows 2008 y estaba usando 'Cualquier CPU')
  • Build de depuración
    • Obtuve la salida correcta tanto F5 como CTRL + F5
  • Release Build
    • Salida correcta con depurador adjunto
    • Sin depurador - Obtuve la salida incorrecta

Entonces, es el JIT x86 que genera incorrectamente el código. He eliminado mi texto original sobre la reordenación de bucles, etc. Algunas otras respuestas aquí han confirmado que el JIT está desenrollando el bucle incorrectamente cuando está en x86.

Para solucionar el problema, puede cambiar la declaración de IntVec a una clase y funciona en todos los sabores.

Creo que esto debe ir en MS Connect ...

-1 a Microsoft!

Andras Zoltan
fuente
1
Idea interesante, pero seguramente esto no es "optimización" sino un error muy importante en el compilador si este es el caso. Habría sido encontrado por ahora, ¿no?
David M
Estoy de acuerdo contigo. Reordenar bucles como este podría causar problemas incalculables. En realidad, esto parece aún menos probable, porque los bucles for nunca pueden llegar a 2.
Andras Zoltan
2
Parece uno de estos desagradables Heisenbugs: P
arul
Cualquier CPU no funcionará si el OP (o cualquiera que use su aplicación) tiene una máquina x86 de 32 bits. El problema es que el x86 JIT con optimizaciones habilitadas genera un código incorrecto.
Nick Guerrera el