Dlaczego kompilator C # tłumaczy to porównanie! = Tak, jakby to było> porównanie?

147

Przez czysty przypadek odkryłem, że kompilator C # włącza tę metodę:

static bool IsNotNull(object obj)
{
    return obj != null;
}

… Do tego CIL :

.method private hidebysig static bool IsNotNull(object obj) cil managed
{
    ldarg.0   // obj
    ldnull
    cgt.un
    ret
}

… Lub, jeśli wolisz spojrzeć na zdekompilowany kod C #:

static bool IsNotNull(object obj)
{
    return obj > null;   // (note: this is not a valid C# expression)
}

Jak to się dzieje, że !=jest tłumaczony jako „ >”?

stakx - już nie wnoszący wkładu
źródło

Odpowiedzi:

201

Krótka odpowiedź:

W języku IL !=nie ma instrukcji „porównaj-nie-równe”, więc operator C # nie ma dokładnej zgodności i nie można go dosłownie przetłumaczyć.

Istnieje jednak instrukcja „porównaj równe” ( ceqbezpośrednia zgodność z ==operatorem), więc w ogólnym przypadku x != yjest tłumaczona jak jej nieco dłuższy odpowiednik (x == y) == false.

W IL ( ) jest również instrukcja "porównaj-większe-niż", cgtktóra pozwala kompilatorowi na użycie pewnych skrótów (tj. Wygenerowanie krótszego kodu IL), jednym z nich jest to, że porównania nierówności obiektów względem wartości null obj != null, są tłumaczone tak, jakby były " obj > null”.

Przejdźmy do bardziej szczegółowych informacji.

Jeśli w IL nie ma instrukcji „porównaj-nie-równe”, w jaki sposób następująca metoda zostanie przetłumaczona przez kompilator?

static bool IsNotEqual(int x, int y)
{
    return x != y;
}

Jak już wspomniano powyżej, kompilator zamieni plik x != yna (x == y) == false:

.method private hidebysig static bool IsNotEqual(int32 x, int32 y) cil managed 
{
    ldarg.0   // x
    ldarg.1   // y
    ceq
    ldc.i4.0  // false
    ceq       // (note: two comparisons in total)
    ret
}

Okazuje się, że kompilator nie zawsze tworzy ten dość długi wzorzec. Zobaczmy, co się stanie, gdy zastąpimy ystałą 0:

static bool IsNotZero(int x)
{
    return x != 0;
}

Wytworzony IL jest nieco krótszy niż w ogólnym przypadku:

.method private hidebysig static bool IsNotZero(int32 x) cil managed 
{
    ldarg.0    // x
    ldc.i4.0   // 0
    cgt.un     // (note: just one comparison)
    ret
}

Kompilator może wykorzystać fakt, że liczby całkowite ze znakiem są przechowywane w uzupełnieniu do dwóch (gdzie, jeśli wynikowe wzorce bitowe są interpretowane jako liczby całkowite bez znaku - to właśnie .unoznacza - 0 ma najmniejszą możliwą wartość), więc przekłada się x == 0tak, jakby była unchecked((uint)x) > 0.

Okazuje się, że kompilator może zrobić to samo w przypadku sprawdzania nierówności względem null:

static bool IsNotNull(object obj)
{
    return obj != null;
}

Kompilator produkuje prawie taki sam IL jak dla IsNotZero:

.method private hidebysig static bool IsNotNull(object obj) cil managed 
{
    ldarg.0
    ldnull   // (note: this is the only difference)
    cgt.un
    ret
}

Najwyraźniej kompilator może założyć, że wzorzec bitowy nullodniesienia jest najmniejszym możliwym wzorcem bitowym dla dowolnego odniesienia do obiektu.

Skrót ten jest wyraźnie wymieniony w dokumencie Common Language Infrastructure Annotated Standard (wydanie 1 z października 2003 r.) (Na stronie 491, jako przypis w tabeli 6-4, „Binary Comparisons or Branch Operations”):

cgt.unjest dozwolone i weryfikowalne w ObjectRefs (O). Jest to często używane podczas porównywania ObjectRef z wartością null (nie ma instrukcji„ porównaj-nie równa ”, która w innym przypadku byłaby bardziej oczywistym rozwiązaniem).”

stakx - już nie wnoszący wkładu
źródło
3
Doskonała odpowiedź, tylko jedna nitka: uzupełnienie do dwóch nie ma tutaj znaczenia. Ważne jest tylko to, że liczby całkowite ze znakiem są przechowywane w taki sposób, że nieujemne wartości w zakresie int'mają taką samą reprezentację intjak w uint. To znacznie słabszy wymóg niż uzupełnienie do dwóch.
3
Typy bez znaku nigdy nie mają żadnych liczb ujemnych, więc operacja porównania, która porównuje do zera, nie może traktować żadnej liczby niezerowej jako mniejszej od zera. Wszystkie reprezentacje odpowiadające nieujemnym wartościom intzostały już przyjęte przez tę samą wartość w uint, więc wszystkie reprezentacje odpowiadające wartościom ujemnym intmuszą odpowiadać jakiejś wartości uintwiększej niż 0x7FFFFFFF, ale tak naprawdę nie ma znaczenia, która wartość to jest. (Właściwie wszystko, co jest naprawdę wymagane, to to, że zero jest reprezentowane w ten sam sposób w obu inti uint.)
3
@hvd: Dzięki za wyjaśnienie. Masz rację, nie liczy się dopełnienie dwóch; jest to wymóg , o którym wspomniałeś oraz fakt, który cgt.untraktuje intjako an uintbez zmiany bazowego wzoru bitowego. (Wyobraź sobie, że cgt.unnajpierw spróbuje niedomiarów fix poprzez mapowanie wszystkich liczb ujemnych na 0. W takim przypadku oczywiście nie może zastąpić > 0w != 0.)
stakx - nie przyczyniając się
2
Zaskakujące jest, że porównanie odwołania do obiektu z innym >używanym jest weryfikowalnym IL. W ten sposób można porównać dwa niezerowe obiekty i otrzymać wynik boolowski (który jest niedeterministyczny). Nie jest to kwestia bezpieczeństwa pamięci, ale wydaje się, że projekt jest nieczysty, który nie jest w ogólnym duchu bezpiecznego kodu zarządzanego. Ten projekt przecieka fakt, że odniesienia do obiektów są implementowane jako wskaźniki. Wygląda na to, że wada projektowa .NET CLI.
usr
3
@usr: Oczywiście! Sekcja III.1.1.4 standardu CLI mówi, że „Odniesienia do obiektów (typ O) są całkowicie nieprzejrzyste” i „jedyne dozwolone operacje porównania to równość i nierówność…”. Być może dlatego, referencje obiektów są nie zdefiniowane w kategoriach adresów pamięci, a średnia dba również do koncepcyjnie zachować odniesienie zerowego oprócz 0 (patrz np definicje ldnull, initobji newobj). Zatem użycie cgt.undo porównywania odniesień do obiektów z odniesieniami zerowymi wydaje się być sprzeczne z sekcją III.1.1.4 na więcej niż jeden sposób.
stakx - już nie publikuje