2010-01-13 21 views
381

El siguiente código proporciona resultados diferentes cuando ejecuta la versión dentro de Visual Studio y ejecuta la versión fuera de Visual Studio. Estoy usando Visual Studio 2008 y estoy apuntando a .NET 3.5. También probé .NET 3.5 SP1..NET JIT ¿error potencial?

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

de salida cuando se ejecuta dentro de Visual Studio:

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

de salida cuando se ejecuta la liberación fuera de Visual Studio:

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

¿Cuál es el motivo?

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(); 
     } 
    } 
} 
+14

+1 - qué gran problema es éste :) –

+8

Sí, qué tal eso: encontrar un error grave en algo tan esencial como el .NET JIT - ¡felicitaciones! –

+73

Esto parece reproducirse en mi versión del 9 de diciembre del framework 4.0 en x86. Lo pasaré al equipo de jitter. ¡Gracias! –

Respuesta

202

Es un error JIT optimizador. Se desenrollar el bucle interno, pero no actualizar 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 se deja incremento oVec.y a 4, que es demasiadas llamadas a desenrollar.

Una solución es la siguiente:

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

ACTUALIZACIÓN: re-comprobado en agosto de 2012, este error se corrigió en la versión 4.0.30319 fluctuación. Pero todavía está presente en el jitter v2.0.50727. Parece poco probable que arreglen esto en la versión anterior después de tanto tiempo.

+11

+1 Definitivamente es un error. Publiqué más o menos la misma respuesta que una edición de mi respuesta mientras esto aparecía. Parece que responder preguntas aquí es a menudo una carrera ... –

+3

+1, definitivamente un error: podría haber identificado las condiciones para el error (sin embargo, ¡no dice que nobugz lo encontró por mi culpa!), pero este (y el tuyo, Nick, así que +1 para ti también) muestra que el JIT es el culpable. Es interesante que la optimización sea eliminada o diferente cuando IntVec se declara como una clase. Incluso si inicializa explícitamente los campos de estructura a 0 primero antes del ciclo, se ve el mismo comportamiento. desagradable! –

+3

@ Hans Passant ¿Qué herramienta usaste para generar el código de ensamblaje? –

22

He copiado el código en una nueva aplicación de consola.

  • generación de depuración
    • salida correcta tanto con depurador y ningún depurador
  • conmutada a Release Build
    • Una vez más, la salida correcta en ambas ocasiones
  • Creado Un nueva configuración x86 (estoy en runnin g X64 Windows 2008 y estaba usando "Cualquier CPU)
  • depuración Construir
    • consiguió la salida correcta tanto F5 y CTRL + F5
  • Release Build
    • salida correcta con el depurador asociado
    • Sin depurador - consiguió la salida incorrecta

Por lo tanto, el JIT x86 genera incorrectamente el código. He eliminado mi texto original sobre el reordenamiento de bucles, etc. Algunas otras respuestas aquí confirmaron 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 colores.

creo que esto tiene que ir en MS Conectar ....

-1 a Microsoft!

+1

Una idea interesante, pero seguramente esto no es una "optimización" sino un error muy importante en el compilador si este es el caso? Se habría encontrado a estas alturas, ¿no? –

+0

Estoy de acuerdo contigo. Reordenar bucles como este podría causar problemas incalculables. En realidad, esto parece incluso menos probable, porque los bucles for nunca pueden llegar a 2. –

+2

Parece uno de estos desagradables Heisenbugs: P – arul

79

Creo que esto es un error de compilación JIT genuino. Me gustaría informar a Microsoft y ver 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 se ve como una optimización ido mal a mí ...