Почему компилятор C# переводит это сравнение != как сравнение ›?

Я случайно обнаружил, что компилятор 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)
}

Почему != переводится как ">"?


person stakx - no longer contributing    schedule 28.02.2015    source источник


Ответы (1)


Короткий ответ:

В IL нет инструкции «сравнить не равно», поэтому оператор C# != не имеет точного соответствия и не может быть переведен буквально.

Однако существует инструкция сравнения-равно (ceq, прямое соответствие оператору ==), поэтому в общем случае x != y переводится как его немного более длинный эквивалент (x == y) == false.

Также в IL есть инструкция сравнения с большим, чем (cgt), которая позволяет компилятору использовать определенные сокращения (например, генерировать более короткий код IL). получить перевод, как если бы они были 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;
}

Полученный IL несколько короче, чем в общем случае:

.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;
}

Компилятор выдает почти такой же IL, как и для IsNotZero:

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

По-видимому, компилятору разрешено предположить, что битовая комбинация ссылки null является наименьшей битовой комбинацией, возможной для любой ссылки на объект.

Этот ярлык явно упоминается на Common Аннотированный стандарт языковой инфраструктуры (1-е издание от октября 2003 г.) (на странице 491, в качестве сноски к Таблице 6-4, Двоичные сравнения или операции ветвления):

cgt.un разрешено и может быть проверено в ObjectRefs (O). Это обычно используется при сравнении ObjectRef с нулевым значением (отсутствует инструкция сравнения неравно, которая в противном случае была бы более очевидным решением).

person stakx - no longer contributing    schedule 28.02.2015
comment
Отличный ответ, всего одна нит: дополнение до двух здесь не имеет значения. Имеет значение только то, что целые числа со знаком хранятся таким образом, что неотрицательные значения в диапазоне int имеют то же представление в int, что и в uint. Это гораздо более слабое требование, чем два дополнения. - person ; 28.02.2015
comment
Типы без знака никогда не имеют отрицательных чисел, поэтому операция сравнения, сравнивающая с нулем, не может рассматривать любое ненулевое число как меньшее нуля. Все представления, соответствующие неотрицательным значениям int, уже заняты одним и тем же значением в uint, поэтому все представления, соответствующие отрицательным значениям int, должны соответствовать некоторым значениям uint, превышающим 0x7FFFFFFF, но на самом деле не имеет значения, какое это значение. (На самом деле все, что действительно требуется, это чтобы ноль представлялся одинаково как в int, так и в uint.) - person ; 28.02.2015
comment
@hvd: Спасибо за объяснение. Вы правы, важно не дополнение двух; это требование о котором вы упомянули и тот факт, что cgt.un обрабатывает int как uint без изменения базового битового шаблона. (Представьте, что cgt.un сначала попытается исправить потери значимости, сопоставив все отрицательные числа с 0. В этом случае вы, очевидно, не сможете заменить > 0 на != 0.) - person stakx - no longer contributing; 28.02.2015
comment
Хех, да, хорошее замечание, это действительно одно требование, о котором я забыл упомянуть. :) - person ; 28.02.2015
comment
Я нахожу удивительным, что сравнение ссылки на объект с другой с использованием > является проверяемым IL. Таким образом можно было бы сравнить два ненулевых объекта и получить логический результат (который не является детерминированным). Это не проблема безопасности памяти, но это похоже на нечистый дизайн, который не соответствует общему духу безопасного управляемого кода. Этот дизайн пропускает тот факт, что ссылки на объекты реализованы как указатели. Похоже на недостаток дизайна .NET CLI. - person usr; 01.03.2015
comment
@usr: Абсолютно! В разделе III.1.1.4 стандарта командной строки говорится, что ссылки на объекты (тип O) полностью непрозрачны. и что разрешены только операции сравнения на равенство и неравенство…. Возможно, поскольку ссылки на объекты не определяются в терминах адресов памяти, стандарт также заботится о том, чтобы концептуально держите нулевую ссылку отдельно от 0 (см., например, определения ldnull, initobj и newobj). Таким образом, использование cgt.un для сравнения ссылок на объекты с нулевыми ссылками противоречит разделу III.1.1.4 более чем одним образом. - person stakx - no longer contributing; 01.03.2015
comment
Альтернативой ceq; ldc.i4.0; ceq; для общего случая не-равно является ceq; ldc.i4.1; xor;. Последнее может быть более подходящим в некоторых случаях; обязательно проверьте выпуск + оптимизированный вывод вашего целевого JIT, чтобы увидеть, как встраивание влияет на собственный поток инструкций. - person Glenn Slayden; 14.12.2018