Dlaczego wartość wyliczenia z tablicy wielowymiarowej nie jest sobie równa?


151

Rozważać:

using System;

public class Test
{
    enum State : sbyte { OK = 0, BUG = -1 }

    static void Main(string[] args)
    {
        var s = new State[1, 1];
        s[0, 0] = State.BUG;
        State a = s[0, 0];
        Console.WriteLine(a == s[0, 0]); // False
    }
}

Jak można to wytłumaczyć? Występuje w kompilacjach debugowania w programie Visual Studio 2015 podczas uruchamiania w JIT x86. Wersja kompilacji lub działająca w JIT x64 drukuje True zgodnie z oczekiwaniami.

Aby odtworzyć z wiersza poleceń:

csc Test.cs /platform:x86 /debug

( /debug:pdbonly, /debug:portableA /debug:fulltakże reprodukować.)


2
ideone.com/li3EzY to prawda. dodaj więcej informacji o wersji .net, IDE, kompilatorze
Backs

1
To samo tutaj. Ale po majstrowaniu przy ustawieniach projektu doszedłem do wniosku, że odznaczenie pola wyboru „Preferuj 32-bitowy” w zakładce „Kompiluj” sprawia, że ​​działa to zgodnie z przeznaczeniem - zwraca wartość true. Tak więc wygląda to dla mnie na problem z WoW64.
Dmitry Rotay

2
Wygląda na to, że wskazałeś błąd we frameworku.
Fabien PERRONNET

1
Co ciekawe, uruchamiając uszkodzony przez kod ildasm, a następnie ilasm„naprawia” to ...
Jon Skeet

2
Na /debug=IMPLliście flag to połamane; /debug=OPT„naprawia” to.
Jon Skeet

Odpowiedzi:


163

Znalazłeś błąd generowania kodu w jitterze .NET 4 x86. Jest to bardzo nietypowe, zawodzi tylko wtedy, gdy kod nie jest zoptymalizowany. Kod maszynowy wygląda następująco:

        State a = s[0, 0];
013F04A9  push        0                            ; index 2 = 0
013F04AB  mov         ecx,dword ptr [ebp-40h]      ; s[] reference
013F04AE  xor         edx,edx                      ; index 1 = 0
013F04B0  call        013F0058                     ; eax = s[0, 0]
013F04B5  mov         dword ptr [ebp-4Ch],eax      ; $temp1 = eax 
013F04B8  movsx       eax,byte ptr [ebp-4Ch]       ; convert sbyte to int
013F04BC  mov         dword ptr [ebp-44h],eax      ; a = s[0, 0]
        Console.WriteLine(a == s[0, 0]); // False
013F04BF  mov         eax,dword ptr [ebp-44h]      ; a
013F04C2  mov         dword ptr [ebp-50h],eax      ; $temp2 = a
013F04C5  push        0                            ; index 2 = 0
013F04C7  mov         ecx,dword ptr [ebp-40h]      ; s[] reference 
013F04CA  xor         edx,edx                      ; index 1 = 0
013F04CC  call        013F0058                     ; eax = s[0, 0]
013F04D1  mov         dword ptr [ebp-54h],eax      ; $temp3 = eax 
                                               ; <=== Bug here!
013F04D4  mov         eax,dword ptr [ebp-50h]      ; a == s[0, 0] 
013F04D7  cmp         eax,dword ptr [ebp-54h]  
013F04DA  sete        cl  
013F04DD  movzx       ecx,cl  
013F04E0  call        731C28F4  

Trudna sprawa z wieloma elementami tymczasowymi i powielaniem kodu, to normalne w przypadku niezoptymalizowanego kodu. Godna uwagi jest instrukcja pod numerem 013F04B8, czyli miejsce, w którym zachodzi konieczna konwersja z bajtu na 32-bitową liczbę całkowitą. Funkcja pomocnicza pobierająca tablicę zwróciła wartość 0x0000000FF, równą State.BUG, która musi zostać przekonwertowana na -1 (0xFFFFFFFF), zanim wartość będzie mogła zostać porównana. Instrukcja MOVSX jest instrukcją Sign eXtension.

To samo dzieje się ponownie w 013F04CC, ale tym razem nie ma instrukcji MOVSX, która wykonałaby taką samą konwersję. Tam właśnie spadają chipy, instrukcja CMP porównuje 0xFFFFFFFF z 0x000000FF i to jest fałsz. Jest to więc błąd pominięcia, generator kodu nie wyemitował ponownie MOVSX, aby wykonać tę samą konwersję sbyte na int.

Szczególnie niezwykłe w tym błędzie jest to, że działa on poprawnie po włączeniu optymalizatora, teraz wie, że używa MOVSX w obu przypadkach.

Prawdopodobnym powodem, dla którego ten błąd pozostawał niewykryty przez tak długi czas, jest użycie sbyte jako podstawowego typu wyliczenia. Dość rzadkie. Korzystanie z wielowymiarowej tablicy ma również znaczenie instrumentalne, połączenie jest fatalne.

W przeciwnym razie powiedziałbym, że jest to dość krytyczny błąd. Trudno zgadnąć, jak powszechne może to być, do przetestowania mam tylko jitter 4.6.1 x86. Jitter x64 i 3.5 x86 generują bardzo różny kod i pozwalają uniknąć tego błędu. Tymczasowym obejściem, aby kontynuować, jest usunięcie sbyte jako podstawowego typu wyliczenia i pozostawienie go domyślnym, int , więc nie jest potrzebne żadne rozszerzenie znaku.

Możesz zgłosić błąd pod adresem connect.microsoft.com, łącze do tego pytania i odpowiedzi powinno wystarczyć, aby przekazać im wszystko, co powinni wiedzieć. Daj mi znać, jeśli nie chcesz tracić czasu, a ja się tym zajmę.


33
Dobre, solidne dane z dokładną przyczyną tak dziwnego problemu, zawsze przyjemne do przeczytania, +1.
Lasse V. Karlsen

11
Opublikuj łącze do artykułu connect.microsoft.com, abyśmy mogli na niego zagłosować.
Hans Passant

Zakładam, że używanie bytezamiast tego również sbytepowinno być w porządku i może być preferowane, jeśli prawdziwy kod jest używany z, powiedzmy, ORM, w którym nie chcesz, aby Twoje flagi w bazie danych zajmowały dodatkowe miejsce.
Voo,

6
Opublikowałbym błąd na dotnet / coreclr zamiast łączyć się, dostaniesz się bezpośrednio do programistów JIT.
Lucas Trzesniewski

8
Jestem programistą w zespole JIT w firmie Microsoft. Odtworzyłem błąd i otworzyłem go wewnętrznie (wysyłka x86 JIT nie jest jeszcze otwarta na github). Jeśli chodzi o czas, kiedy to zostanie naprawione, spodziewam się, że będziemy mieć tę poprawkę zawartą w następnej głównej wersji narzędzi. Jeśli ten błąd ma wpływ na działalność biznesową i potrzebujesz wcześniejszej naprawy, zgłoś problem z połączeniem (connect.microsoft.com), abyśmy mogli sprawdzić wpływ i jakie alternatywy mamy, aby szybciej rozwiązać problem.
Russell C. Hadley

8

Rozważmy deklarację OP:

enum State : sbyte { OK = 0, BUG = -1 }

Ponieważ błąd występuje tylko wtedy, gdy BUGjest ujemny (od -128 do -1), a Stan jest wyliczeniem bajtu ze znakiem, zacząłem przypuszczać, że gdzieś wystąpił problem z rzutowaniem.

Jeśli uruchomisz to:

Console.WriteLine((sbyte)s[0, 0]);
Console.WriteLine((sbyte)State.BUG);
Console.WriteLine(s[0, 0]);
unchecked
{
    Console.WriteLine((byte) State.BUG);
}

wyświetli:

255

-1

PLUSKWA

255

Z powodu, który ignoruję (na razie) s[0, 0] jest rzutowany na bajt przed oceną i dlatego twierdzi, że a == s[0,0]jest fałszywy.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.