Порядок аргумента для '==' с Nullable<T>

Следующие два C# функции отличаются только переключением левого / правого порядка аргументов на оператор равенства, ==, (Тип IsInitialized является bool). Использование C# 7.1 и .NET 4.7.

static void A(ISupportInitialize x)
{
    if ((x as ISupportInitializeNotification)?.IsInitialized == true)
        throw null;
}

static void B(ISupportInitialize x)
{
    if (true == (x as ISupportInitializeNotification)?.IsInitialized)
        throw null;
}

Но код IL для второго кажется гораздо более сложным. Например, B это:

  • На 36 байт длиннее (код IL);
  • вызывает дополнительные функции, включая newobj а также initobj;
  • объявляет четырех местных жителей против одного.

IL для функции 'A'…

[0] bool flag
        nop
        ldarg.0
        isinst [System]ISupportInitializeNotification
        dup
        brtrue.s L_000e
        pop
        ldc.i4.0
        br.s L_0013
L_000e: callvirt instance bool [System]ISupportInitializeNotification::get_IsInitialized()
L_0013: stloc.0
        ldloc.0
        brfalse.s L_0019
        ldnull
        throw
L_0019: ret

IL для функции 'B'…

[0] bool flag,
[1] bool flag2,
[2] valuetype [mscorlib]Nullable`1<bool> nullable,
[3] valuetype [mscorlib]Nullable`1<bool> nullable2
        nop
        ldc.i4.1
        stloc.1
        ldarg.0
        isinst [System]ISupportInitializeNotification
        dup
        brtrue.s L_0018
        pop
        ldloca.s nullable2
        initobj [mscorlib]Nullable`1<bool>
        ldloc.3
        br.s L_0022
L_0018: callvirt instance bool [System]ISupportInitializeNotification::get_IsInitialized()
        newobj instance void [mscorlib]Nullable`1<bool>::.ctor(!0)
L_0022: stloc.2
        ldloc.1
        ldloca.s nullable
        call instance !0 [mscorlib]Nullable`1<bool>::GetValueOrDefault()
        beq.s L_0030
        ldc.i4.0
        br.s L_0037
L_0030: ldloca.s nullable
        call instance bool [mscorlib]Nullable`1<bool>::get_HasValue()
L_0037: stloc.0
        ldloc.0
        brfalse.s L_003d
        ldnull
        throw
L_003d: ret

Quesions

  1. Есть ли какая-либо функциональная, семантическая или другая существенная разница во времени выполнения между A и B? (Здесь нас интересует только правильность, а не производительность)
  2. Если они не являются функционально эквивалентными, каковы условия выполнения, которые могут выявить заметную разницу?
  3. Если они являются функциональными эквивалентами, что делает B (что всегда заканчивается тем же результатом, что и A), и что вызвало его спазм? Есть ли в B ветки, которые никогда не могут выполняться?
  4. Если разница объясняется разницей между тем, что появляется на левой стороне ==(здесь свойство, ссылающееся на выражение, а не на буквальное значение), можете ли вы указать раздел спецификации C#, который описывает детали.
  5. Существует ли надежное эмпирическое правило, которое можно использовать для предсказания раздутого IL во время кодирования и, таким образом, избежать его создания?

БОНУС. Как выглядит соответствующий финал JITted x86 или же AMD64 код для каждого стека?


[редактировать]
Дополнительные заметки основаны на отзывах в комментариях. Сначала был предложен третий вариант, но он дает идентичный IL как A (для обоих Debug а также Release строит). Однако, по-новому, C# для нового кажется более гладким, чем A:

static void C(ISupportInitialize x)
{
    if ((x as ISupportInitializeNotification)?.IsInitialized ?? false)
        throw null;
}

Здесь также есть Release IL для каждой функции. Обратите внимание, что асимметрия A/C и B по-прежнему очевидна с Release Ил, так что оригинальный вопрос все еще стоит.

Отпустите IL для функций 'A', 'C'…

        ldarg.0
        isinst [System]ISupportInitializeNotification
        dup
        brtrue.s L_000d
        pop
        ldc.i4.0
        br.s L_0012
L_000d: callvirt instance bool [System]ISupportInitializeNotification::get_IsInitialized()
        brfalse.s L_0016
        ldnull
        throw
L_0016: ret

Отпустите IL для функции 'B'…

[0] valuetype [mscorlib]Nullable`1<bool> nullable,
[1] valuetype [mscorlib]Nullable`1<bool> nullable2
        ldc.i4.1
        ldarg.0
        isinst [System]ISupportInitializeNotification
        dup
        brtrue.s L_0016
        pop
        ldloca.s nullable2
        initobj [mscorlib]Nullable`1<bool>
        ldloc.1
        br.s L_0020
L_0016: callvirt instance bool [System]ISupportInitializeNotification::get_IsInitialized()
        newobj instance void [mscorlib]Nullable`1<bool>::.ctor(!0)
L_0020: stloc.0
        ldloca.s nullable
        call instance !0 [mscorlib]Nullable`1<bool>::GetValueOrDefault()
        beq.s L_002d
        ldc.i4.0
        br.s L_0034
L_002d: ldloca.s nullable
        call instance bool [mscorlib]Nullable`1<bool>::get_HasValue()
L_0034: brfalse.s L_0038
        ldnull
        throw
L_0038: ret

Наконец, была упомянута версия, использующая новый синтаксис C# 7, который, кажется, производит самый чистый IL из всех:

static void D(ISupportInitialize x)
{
    if (x is ISupportInitializeNotification y && y.IsInitialized)
        throw null;
}

Отпустите IL для функции 'D'…

[0] class [System]ISupportInitializeNotification y
        ldarg.0 
        isinst [System]ISupportInitializeNotification
        dup 
        stloc.0 
        brfalse.s L_0014
        ldloc.0 
        callvirt instance bool [System]ISupportInitializeNotification::get_IsInitialized()
        brfalse.s L_0014
        ldnull 
        throw 
L_0014: ret 

2 ответа

Похоже, 1-й операнд преобразуется во 2-й тип для сравнения.

Избыточные операции в случае B включают построение Nullable<bool>(true), Хотя в случае А, сравнить что-то с true/falseесть одна инструкция IL (brfalse.s) это делает

Я не смог найти конкретную ссылку в спецификации C# 5.0. 7.10 Операторы реляционного и типового тестирования относятся к 7.3.4 Разрешение перегрузки бинарных операторов, которое, в свою очередь, относится к 7.5.3 разрешению перегрузки, но последнее очень расплывчато.

Поэтому мне было любопытно получить ответ и взглянуть на спецификацию C# 6 (без понятия, где находится спецификация C# 7). Полный отказ от ответственности: я не гарантирую, что мой ответ правильный, потому что я не писал c/ spec/ compiler и мое понимание внутренних возможностей ограничено.

Тем не менее, я думаю, что ответ заключается в разрешении перегрузки == оператор. Лучшая применимая перегрузка для == определяется с помощью правил для лучших членов функции.

Из спецификации:

Имеется список аргументов A с набором выражений аргументов {E1, E2, ..., En} и двумя применимыми членами-функциями Mp и Mq с типами параметров {P1, P2, ..., Pn} и {Q1, Q2, ..., Qn}, Mp определен как лучший член функции, чем Mq, если

для каждого аргумента неявное преобразование из Ex в Qx не лучше, чем неявное преобразование из Ex в Px, и по крайней мере для одного аргумента преобразование из Ex в Px лучше, чем преобразование из Ex в Qx.

То, что привлекло мое внимание, является списком аргументов {E1, E2, .., En}, Если вы сравните Nullable<bool> к bool список аргументов должен быть что-то вроде {Nullable<bool> a, bool b}и для этого аргумента перечислите Nullable<bool>.Equals(object o) Метод, кажется, лучшая функция, потому что он принимает только одно неявное преобразование из bool в object,

Однако, если вы измените порядок списка аргументов на {bool a, Nullable<bool> b} Nullable<bool>.Equals(object o) метод больше не является лучшей функцией, потому что теперь вам придется конвертировать из Nullable<bool> в bool в первом аргументе, а затем из bool в object во втором аргументе. Вот почему для случая A выбрана другая перегрузка, которая, кажется, приводит к более чистому IL-коду.

Опять же, это объяснение, которое удовлетворяет мое собственное любопытство и похоже соответствует спецификации C#. Но мне еще предстоит выяснить, как отладить компилятор, чтобы увидеть, что на самом деле происходит.

Другие вопросы по тегам