Potencjalny błąd .NET JIT?

404

Poniższy kod podaje inne dane wyjściowe podczas uruchamiania wydania w programie Visual Studio i uruchamiania wydania poza programem Visual Studio. Używam Visual Studio 2008 i celuję w .NET 3.5. Próbowałem także .NET 3.5 SP1.

Podczas uruchamiania poza Visual Studio, JIT powinien się uruchomić. Albo (a) dzieje się coś subtelnego z C #, którego mi brakuje lub (b) JIT jest w błędzie. Wątpię, czy JIT może się nie udać, ale brakuje mi innych możliwości ...

Dane wyjściowe podczas uruchamiania w programie Visual Studio:

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

Dane wyjściowe podczas uruchamiania wersji poza Visual Studio:

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

Jaki jest powód?

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
źródło
8
Tak - co powiesz na to: znalezienie poważnego błędu w czymś tak istotnym jak JIT .Net - gratulacje!
Andras Zoltan
73
Wygląda to na repro w mojej 9-tej wersji kompilacji środowiska 4.0 na x86. Przekażę to zespołowi jittera. Dzięki!
Eric Lippert,
28
To jedno z niewielu pytań, które faktycznie zasługują na złotą odznakę.
Mehrdad Afshari
28
Fakt, że wszyscy jesteśmy zainteresowani tym pytaniem pokazuje, że nie oczekujemy błędów w .NET JIT, dobrze zrobiony Microsoft.
Ian Ringrose
2
Wszyscy z niecierpliwością czekamy na odpowiedź Microsoftu ...
Talha,

Odpowiedzi:

211

Jest to błąd optymalizatora JIT. Rozwija wewnętrzną pętlę, ale nie aktualizuje poprawnie wartości oVec.y:

      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 

Błąd znika, gdy pozwalasz zwiększać oVec.y do 4, to zbyt wiele połączeń, aby rozwinąć.

Jednym z obejść tego jest:

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

AKTUALIZACJA: ponownie sprawdzona w sierpniu 2012 r., Ten błąd został naprawiony w wersji jitter 4.0.30319. Ale nadal jest obecny w jitter v2.0.50727. Wydaje się mało prawdopodobne, że naprawią to w starej wersji po tak długim czasie.

Hans Passant
źródło
3
+1, zdecydowanie błąd - mogłem zidentyfikować warunki błędu (nie mówiąc, że nobugz znalazł go przeze mnie!), Ale to (i twoje, Nick, więc +1 również dla ciebie) pokazuje, że JIT jest winowajcą. interesujące jest to, że optymalizacja zostanie usunięta lub inna, gdy IntVec zostanie zadeklarowany jako klasa. Nawet jeśli bezpośrednio zainicjujesz pola struct na 0 przed pętlą, zobaczysz to samo zachowanie. Paskudny!
Andras Zoltan
3
@Hans Passant Jakiego narzędzia użyłeś do wygenerowania kodu asemblera?
3
@Joan - Just Visual Studio, skopiuj / wklej z okna Demontażu debugera i dodaj komentarze ręcznie.
Hans Passant
82

Wierzę, że jest to prawdziwy błąd kompilacji JIT. Zgłosiłbym to do Microsoft i sprawdził, co mówią. Co ciekawe, odkryłem, że JIT x64 nie ma tego samego problemu.

Oto moje czytanie JIT x86.

// 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     

Wygląda na to, że optymalizacja mi się nie udała ...

Nick Guerrera
źródło
23

Skopiowałem twój kod do nowej aplikacji konsoli.

  • Kompilacja debugowania
    • Prawidłowe wyjście zarówno z debuggerem, jak i bez debuggera
  • Przełączono na wersję kompilacji
    • Znowu popraw dane wyjściowe za każdym razem
  • Utworzono nową konfigurację x86 (korzystam z X64 Windows 2008 i używałem „Any CPU”)
  • Kompilacja debugowania
    • Mam poprawne wyjście zarówno F5, jak i CTRL + F5
  • Wersja kompilacji
    • Prawidłowe wyjście z podłączonym debuggerem
    • Brak debuggera - Niepoprawne wyjście

Więc to JIT x86 niepoprawnie generuje kod. Usunąłem mój oryginalny tekst na temat zmiany kolejności pętli itp. Kilka innych odpowiedzi tutaj potwierdziło, że JIT nieprawidłowo odwija ​​pętlę na x86.

Aby rozwiązać problem, możesz zmienić deklarację IntVec na klasę i działa ona we wszystkich smakach.

Pomyśl, że to musi iść na MS Connect ....

-1 do Microsoft!

Andras Zoltan
źródło
1
Ciekawy pomysł, ale na pewno nie jest to „optymalizacja”, ale bardzo poważny błąd w kompilatorze, jeśli tak jest? Czy zostałby już znaleziony, prawda?
David M
Zgadzam się z Tobą. Zmiana kolejności takich pętli może powodować niezliczone problemy. W rzeczywistości wydaje się to jeszcze mniej prawdopodobne, ponieważ pętle for nigdy nie osiągną 2.
Andras Zoltan
2
Wygląda jak jeden z tych paskudnych Heisenbugs: P
arul
Dowolny procesor nie będzie działał, jeśli OP (lub ktokolwiek korzystający z jego aplikacji) ma 32-bitową maszynę x86. Problem polega na tym, że JIT x86 z włączonymi optymalizacjami generuje zły kod.
Nick Guerrera