.NET JIT potenzieller Fehler?

404

Der folgende Code gibt eine andere Ausgabe aus, wenn die Version in Visual Studio und die Version außerhalb von Visual Studio ausgeführt wird. Ich verwende Visual Studio 2008 und ziele auf .NET 3.5 ab. Ich habe auch .NET 3.5 SP1 ausprobiert.

Wenn Sie außerhalb von Visual Studio ausgeführt werden, sollte die JIT aktiviert werden. Entweder (a) mit C # ist etwas Feines los, das mir fehlt, oder (b) die JIT ist tatsächlich fehlerhaft. Ich bin mir nicht sicher, ob die GEG schief gehen kann, aber mir gehen andere Möglichkeiten aus ...

Ausgabe bei Ausführung in Visual Studio:

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

Ausgabe beim Ausführen einer Version außerhalb von Visual Studio:

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

Was ist der Grund?

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
quelle
8
Ja - wie wäre es damit: einen schwerwiegenden Fehler in etwas so Wesentlichem wie dem .Net JIT zu finden - herzlichen Glückwunsch!
Andras Zoltan
73
Dies scheint in meinem Build des 4.0-Frameworks auf x86 vom 9. Dezember zu wiederholen. Ich werde es an das Jitter-Team weitergeben. Vielen Dank!
Eric Lippert
28
Dies ist eine der wenigen Fragen, die tatsächlich ein goldenes Abzeichen verdienen.
Mehrdad Afshari
28
Die Tatsache, dass wir alle an dieser Frage interessiert sind, zeigt, dass wir keine Fehler in der .NET JIT erwarten , gut gemacht, Microsoft.
Ian Ringrose
2
Wir alle warten gespannt auf die Antwort von Microsoft .....
Talha

Antworten:

211

Es ist ein JIT-Optimierungsfehler. Es rollt die innere Schleife ab, aktualisiert aber den oVec.y-Wert nicht richtig:

      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 

Der Fehler verschwindet, wenn Sie oVec.y auf 4 erhöhen lassen. Das sind zu viele Aufrufe zum Abrollen.

Eine Problemumgehung ist folgende:

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

UPDATE: Im August 2012 erneut überprüft, wurde dieser Fehler in der Version 4.0.30319 Jitter behoben. Ist aber noch im v2.0.50727 Jitter vorhanden. Es ist unwahrscheinlich, dass sie dies nach so langer Zeit in der alten Version beheben werden.

Hans Passant
quelle
3
+1, definitiv ein Fehler - Ich hätte vielleicht die Bedingungen für den Fehler identifiziert (ohne zu sagen, dass Nobugz ihn wegen mir gefunden hat!), Aber dies (und dein, Nick, also +1 auch für dich) zeigt, dass die JIT ist der Schuldige. Interessant ist, dass die Optimierung entweder entfernt oder anders ist, wenn IntVec als Klasse deklariert wird. Selbst wenn Sie die Strukturfelder vor der Schleife explizit auf 0 initialisieren, wird dasselbe Verhalten angezeigt. Böse!
Andras Zoltan
3
@Hans Passant Mit welchem ​​Tool haben Sie den Assemblycode ausgegeben?
3
@Joan - Nur Visual Studio, Kopieren / Einfügen aus dem Disassembly-Fenster des Debuggers und manuell hinzugefügte Kommentare.
Hans Passant
82

Ich glaube, das ist ein echter JIT-Kompilierungsfehler. Ich würde es Microsoft melden und sehen, was sie sagen. Interessanterweise stellte ich fest, dass die x64-JIT nicht das gleiche Problem hat.

Hier ist meine Lektüre des 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     

Das sieht nach einer für mich schlechten Optimierung aus ...

Nick Guerrera
quelle
23

Ich habe Ihren Code in eine neue Konsolen-App kopiert.

  • Debug Build
    • Korrigieren Sie die Ausgabe sowohl mit dem Debugger als auch ohne Debugger
  • Auf Release Build umgestellt
    • Wiederum beide Male die Ausgabe korrigieren
  • Erstellt eine neue x86-Konfiguration (ich verwende X64 Windows 2008 und verwende 'Beliebige CPU')
  • Debug Build
    • Habe die richtige Ausgabe sowohl F5 als auch STRG + F5
  • Build freigeben
    • Korrigieren Sie die Ausgabe mit angeschlossenem Debugger
    • Kein Debugger - Falsche Ausgabe

Es ist also die x86-JIT, die den Code falsch generiert. Habe meinen Originaltext über die Neuordnung von Schleifen usw. gelöscht. Einige andere Antworten hier haben bestätigt, dass die JIT die Schleife unter x86 falsch abwickelt.

Um das Problem zu beheben, können Sie die Deklaration von IntVec in eine Klasse ändern, die in allen Varianten funktioniert.

Denken Sie, dass dies auf MS Connect gehen muss ....

-1 an Microsoft!

Andras Zoltan
quelle
1
Interessante Idee, aber dies ist sicherlich keine "Optimierung", sondern ein sehr großer Fehler im Compiler, wenn dies der Fall ist? Wäre inzwischen gefunden worden, nicht wahr?
David M
Ich stimme mit Ihnen ein. Das Neuanordnen solcher Schleifen kann zu unzähligen Problemen führen. Tatsächlich scheint dies noch weniger wahrscheinlich zu sein, da die for-Schleifen niemals 2 erreichen können.
Andras Zoltan
2
Sieht aus wie einer dieser fiesen Heisenbugs: P
arul
Jede CPU funktioniert nicht, wenn das OP (oder jemand, der seine Anwendung verwendet) über einen 32-Bit-x86-Computer verfügt. Das Problem ist, dass die x86-JIT mit aktivierten Optimierungen fehlerhaften Code generiert.
Nick Guerrera