Чому компілятор C # перекладає це! = Порівняння так, ніби це порівняння?


147

Я випадково виявив, що компілятор C # перетворює цей метод:

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

… У цей CIL :

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

… Або, якщо ви хочете переглянути декомпільований код C #:

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

Як так, що !=" >" перекладається як " "?

Відповіді:


201

Коротка відповідь:

В IL немає інструкції "порівняти не рівне", тому !=оператор C # не має точної відповідності і не може бути перекладений буквально.

Однак існує інструкція "порівняти-рівну" ( ceqпряма відповідність ==оператору), тому в загальному випадку x != yперекладається як її трохи довший еквівалент (x == y) == false.

Існує також інструкція "порівняти-перевищувати", ніж в IL ( cgt), яка дозволяє компілятору приймати певні ярлики (тобто генерувати коротший код IL), одна з яких полягає в тому, що порівняння нерівності об'єктів проти нуля, obj != nullпереводиться так, ніби вони " obj > null".

Розглянемо детальніше.

Якщо в IL немає інструкції "порівняти-не-рівну", то як компілятор переведе наступний метод?

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

Як вже було сказано вище, компілятор перетворить x != yна (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
}

Виявляється, компілятор не завжди створює цю досить довгу модель. Давайте подивимося, що станеться, коли ми замінимо yна постійне 0:

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

Отриманий ІЛ дещо коротший, ніж у загальному випадку:

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

Компілятор може скористатися тим, що підписані цілі числа зберігаються в доповненнях двох (де, якщо отримані бітові шаблони інтерпретуються як цілі числа без підпису - це те, що .unозначає - 0 має найменше можливе значення), тому він перекладається x == 0так, ніби unchecked((uint)x) > 0.

Виявляється, компілятор може зробити те саме для перевірки нерівності щодо null:

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

Компілятор видає майже той самий ІЛ, що і для IsNotZero:

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

Мабуть, компілятору дозволено припустити, що бітова модель nullпосилання є найменшим шаблоном бітів, можливим для будь-якого посилання на об'єкт.

Цей ярлик явно згадується в Анотованому стандарті загальної мовної інфраструктури (1-е видання від жовтня 2003 р.) (На стор. 491, як виноска до таблиці 6-4, "Бінарні порівняння або операції з філіями"):

" cgt.unдозволено і перевірено в ObjectRefs (O). Це зазвичай використовується при порівнянні ObjectRef з null (немає інструкції" порівняти-не-рівно ", яка в іншому випадку була б більш очевидним рішенням."


3
Відмінна відповідь, лише одна нитка: доповнення двох тут не доречне. intМає значення лише те, що підписані цілі числа зберігаються таким чином, що негативні значення в діапазоні 's мають таке ж представлення, intяк і в uint. Це набагато слабша вимога, ніж доповнення двох.

3
Непідписані типи ніколи не мають негативних чисел, тому операція порівняння, яка порівнюється з нулем, не може розглядати жодне ненульове число як менше нуля. Усі подання, що відповідають негативним значенням int, вже були прийняті одним і тим самим значенням uint, тому всі подання, що відповідають негативним значенням, intповинні відповідати деякому значенню, що uintперевищує 0x7FFFFFFF, але не має значення, яке значення має є. (Насправді, все, що потрібно насправді, це те, що нуль представлений однаково в обох intі uint.)

3
@hvd: Дякую за пояснення. Ви маєте рацію, значення має не два доповнення; це вимога, яку ви згадали, і той факт, що cgt.unтрактує intяк uintбез змін базового бітового шаблону. (Уявіть собі , що cgt.unб спочатку спробувати виправити втрати значущості шляхом зіставлення всіх негативних чисел в 0. У цьому випадку ви , очевидно , не може замінити > 0для != 0.)
stakx - більше не сприяє

2
Мені здається дивним, що порівнюючи посилання на об'єкт з іншим, що використовується, >це ІЛ, що перевіряється. Таким чином можна порівняти два ненульові об’єкти та отримати бульний результат (що не детерміновано). Це не питання безпеки пам’яті, але здається, що нечистий дизайн не відповідає загальному духу безпечного керованого коду. Ця конструкція просочується тим, що посилання на об'єкти реалізуються як покажчики. Схоже, вада дизайну .NET CLI.
usr

3
@usr: Абсолютно! Розділ III.1.1.4 стандарту CLI говорить, що "Посилання на об'єкти (тип O) є абсолютно непрозорими", і що "єдиними дозволеними операціями порівняння є рівність та нерівність ...". Можливо , тому що посилання на об'єкт НЕ визначені в термінах адрес пам'яті, стандарт також бере на себе концептуально зберегти посилання нульовий відокремлений від 0 (див , наприклад , визначення ldnull, initobjі newobj). Таким чином, використання cgt.unдля порівняння посилань на об'єкти з нульовою посиланням, здається, суперечить розділу III.1.1.4 більш ніж одним способом.
stakx - більше не вносить
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.