Почему логические операторы «и» короткого замыкания не используются при сравнении двух обнуляемых для равенства? - PullRequest
0 голосов
/ 29 октября 2018

У меня есть метод, который сравнивает два обнуляемых числа и выводит результат сравнения на консоль:

static void TestMethod(int? i1, int? i2)
{
    Console.WriteLine(i1 == i2);
}

И это результат его декомпиляции:

private static void TestMethod(int? i1, int? i2)
{
    int? nullable = i1;
    int? nullable2 = i2;
    Console.WriteLine((nullable.GetValueOrDefault() == nullable2.GetValueOrDefault()) & (nullable.HasValue == nullable2.HasValue));
}

Результат более или менее соответствует ожидаемому, но мне интересно, почему вместо версии с коротким замыканием (&&) используется версия логического оператора 'и' без замыкания (&). Мне кажется, что последнее было бы более эффективным - если уже известно, что одна сторона сравнения является ложной, тогда нет необходимости оценивать другую сторону. Нужен ли здесь оператор & или это просто деталь реализации, которая не настолько важна, чтобы о ней беспокоиться?

Ответы [ 2 ]

0 голосов
/ 30 октября 2018

Результат более менее, чем я ожидал, но мне интересно, почему вместо версии с коротким замыканием (&&) используется версия логики и оператора без замыкания (&). Мне кажется, что последняя была бы более эффективной - если одна сторона сравнения уже известна, тогда нет необходимости оценивать другую сторону. Есть ли причина, по которой требуется использование оператора &, или это просто детали реализации, которые не настолько важны, чтобы их беспокоить?

Это отличный вопрос.

Во-первых, я работал над генераторами кода как для кода, предшествующего обнулению, так и для исходной реализации в Roslyn. Это сложный код с множеством возможностей для ошибок и пропущенных оптимизаций. Я написал длинную серию статей в блоге о том, как работает оптимизатор понижаемого значения Roslyn, который начинается здесь:

https://ericlippert.com/2012/12/20/nullable-micro-optimizations-part-one/

Если эта тема вас интересует, эта серия статей, вероятно, очень поможет. Третья часть особенно уместна, поскольку в ней обсуждается связанный с ней вопрос: для арифметики, допускающей значение NULL, мы генерируем (x.HasValue & y.HasValue) ? new int?(x.Value + y.Value) : new int?() или используем && или используем GetValueOrDefault или как? (Ответ, конечно, таков: я попробовал все из них и выбрал тот, который создает самый быстрый наименьший код.) Однако в этой серии не рассматривается ваш конкретный вопрос, касающийся равноправного равенства. У равноправного равенства есть несколько иные правила, чем у обычной поднятой арифметики.

Конечно, я не был в Microsoft с 2012 года, и они могли изменить его с тех пор; Я бы не знал. (ОБНОВЛЕНИЕ: Глядя на связанную проблему в комментариях выше, представляется вероятным, что я пропустил оптимизацию в моей первоначальной реализации в 2011 году и что это было исправлено в 2017 году.)

Чтобы ответить на ваш конкретный вопрос: проблема с && заключается в том, что она дороже , чем &, когда работа, выполняемая с правой стороны оператора, дешевле чем тест-и-ветка. Тест и ветка - это не просто инструкции. Это, очевидно, ветвь, которая обладает множеством эффектов. На уровне процессора ответвления требуют прогнозирования ветвлений, а ответвления могут быть предсказаны неправильно. Ветви означают более простые блоки, и помните, что оптимизатор jit работает во время выполнения , что означает, что оптимизатор jit должен быть fast . Разрешается говорить «слишком много базовых блоков в этом методе, я собираюсь пропустить некоторые оптимизации», и поэтому, возможно, добавление дополнительных базовых блоков неоправданно плохо.

Короче говоря, компилятор C # будет сгенерировать операции "и" как активные, если правая сторона не имеет побочных эффектов, и компилятор считает, что оценка left & right будет быстрее и короче, чем оценка left ? right : false. Часто стоимость оценки right настолько низкая, что стоимость ветки на дороже , чем простая арифметика.

Вы можете увидеть это в областях, отличных от обнуляемого оптимизатора; например, если у вас есть

bool x = X();
bool y = Y();
bool z = x && y;

, тогда это будет сгенерировано как z = x & y, потому что компилятор знает, что нет дорогостоящей операции для сохранения; Y() уже уже вызвано.

0 голосов
/ 29 октября 2018

Если вы посмотрите на скомпилированный CIL-код (Roslyn 2.0), вы действительно увидите (строка IL_0016) инструкцию br.s, которая выполняет короткое замыкание при вызове Console.WriteLine(), если результаты методов GetValue() не верны. не равны (IL_0013). Я полагаю, что ваш декомпилятор не заботится о том, чтобы правильно отобразить его как &&.

IL_0000:  nop
IL_0001:  ldarg.0
IL_0002:  stloc.0
IL_0003:  ldarg.1
IL_0004:  stloc.1
IL_0005:  ldloca.s   V_0
IL_0007:  call       instance !0 valuetype [mscorlib]System.Nullable`1<int32>::GetValueOrDefault()
IL_000c:  ldloca.s   V_1
IL_000e:  call       instance !0 valuetype [mscorlib]System.Nullable`1<int32>::GetValueOrDefault()
IL_0013:  beq.s      IL_0018

IL_0015:  ldc.i4.0
IL_0016:  br.s       IL_0028

IL_0018:  ldloca.s   V_0
IL_001a:  call       instance bool valuetype [mscorlib]System.Nullable`1<int32>::get_HasValue()
IL_001f:  ldloca.s   V_1
IL_0021:  call       instance bool valuetype [mscorlib]System.Nullable`1<int32>::get_HasValue()
IL_0026:  ceq
IL_0028:  call       void [mscorlib]System.Console::WriteLine(bool)
IL_002d:  nop
IL_002e:  ret
...