411 Stimmen

.NET 3.5 JIT funktioniert nicht, wenn die Anwendung ausgeführt wird

Der folgende Code liefert unterschiedliche Ausgaben, wenn die Version innerhalb von Visual Studio und außerhalb von Visual Studio ausgeführt wird. Ich verwende Visual Studio 2008 und ziele auf .NET 3.5. Ich habe auch versucht, .NET 3.5 SP1.

Bei der Ausführung außerhalb von Visual Studio sollte das JIT aktiviert werden. Entweder (a) gibt es etwas subtiles vor sich geht mit C #, die ich vermisse oder (b) die JIT ist tatsächlich in Fehler. Ich bin zweifelhaft, dass die JIT falsch gehen kann, aber ich bin aus anderen Möglichkeiten...

Ausgabe bei Ausführung in Visual Studio:

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

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

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

Was ist der Grund dafür?

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

211voto

Hans Passant Punkte 894572

Es handelt sich um einen Fehler im JIT-Optimierer. Er 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 inkrementieren lassen, das sind zu viele Aufrufe zum Abrollen.

Eine Abhilfe kann folgendermaßen aussehen:

  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. Aber ist immer noch in der v2.0.50727 Jitter vorhanden. Es scheint unwahrscheinlich, dass sie dieses Problem in der alten Version nach so langer Zeit beheben werden.

81voto

Nick Guerrera Punkte 2529

Ich glaube, es handelt sich um einen echten JIT-Kompilierungsfehler. Ich würde es Microsoft melden und abwarten, was sie sagen. Interessanterweise habe ich festgestellt, dass die x64 JIT nicht das gleiche Problem haben.

Hier ist meine Lesart 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 für mich nach einer missglückten Optimierung aus...

23voto

Andras Zoltan Punkte 41403

Ich habe Ihren Code in eine neue Console App kopiert.

  • Debug Build
    • Korrekte Ausgabe mit Debugger und ohne Debugger
  • Zu Release Build gewechselt
    • Wiederum korrekte Ausgabe in beiden Fällen
  • Erstellen Sie eine neue x86-Konfiguration (ich verwende X64 Windows 2008 und benutzte 'Any CPU')
  • Debug Build
    • Die Ausgabe ist sowohl bei F5 als auch bei STRG+F5 korrekt.
  • Release Build
    • Korrekte Ausgabe mit angeschlossenem Debugger
    • Kein Debugger - Ich habe die falsche Ausgabe

Es ist also das x86 JIT, das den Code falsch generiert. Ich habe meinen ursprünglichen Text über die Neuanordnung von Schleifen usw. gelöscht. Ein paar andere Antworten hier haben bestätigt, dass das JIT die Schleife falsch abwickelt, wenn auf x86.

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

Ich denke, das muss auf MS Connect.... veröffentlicht werden.

-1 an Microsoft!

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X