#c #boolean #comparison-operators #boolean-operations
#c #логическое #операторы сравнения #логические операции
Вопрос:
В C существуют логические операторы amp;amp;
, ||
, !
, которые соответствуют соединению, дизъюнкции, отрицанию соответственно.
Но я заметил, что операторы сравнения ==
, !=
, <
, >
, <=
, >=
могут использоваться и для логических значений! Учитывая, что P
и Q
являются логическими значениями:
P == Q
является двузначным,
P != Q
является исключающей дизъюнкцией,
P < Q
является ли обратное неимпликацией,
P > Q
является неимпликацией,
P <= Q
является следствием,
И P >= Q
является обратным подтекстом .
Мои вопросы:
- Будет ли программа работать лучше с помощью этого трюка?
- Есть ли какой-нибудь пример кода, использующего этот трюк (на любом языке)?
Комментарии:
1. Что с приведениями? Почему бы и нет
P < Q
,P > Q
,P <= Q
,P >= Q
?2. О, я не знал, что <, >, <=, >= можно использовать и для логических значений. Спасибо.
3. «Увеличится ли производительность благодаря этому трюку?» Увеличение по сравнению с чем?
4. Скорость. Разве это не было очевидно?
5. Я имею в виду по сравнению с использованием только кода
amp;amp;
,||
и!
?
Ответ №1:
Увеличит ли производительность этот трюк?
На любом процессоре, где это имело бы какую-либо выгоду, когда P
и Q
являются простыми переменными, это было бы достаточно просто, чтобы вы могли ожидать, что компиляторы будут использовать его уже без необходимости переписывания исходного кода.
Но имейте в виду, что P < Q
в целом это имеет явный недостаток !P amp;amp; Q
: это требует оценки Q
, когда результат уже известен, если P
вычисляется true
. То же самое относится ко всем другим операторам отношения.
Есть ли какой-нибудь пример кода, использующего этот трюк (на любом языке)?
Не как трюк, а потому, что это, возможно, приводит к более понятному коду (а не к какому-либо конкретному языку):
if ((a == null) != (b == null))
throw "a and b must either both be null, or both be non-null";
Это может быть написано с ^
помощью . Что легче читать, зависит от мнения.
Комментарии:
1. Из-за недостатка операторов сравнения по сравнению с логическими операторами я опубликовал предложение по оптимизации в предложении ISO C .
2. Удачи, но, честно говоря, я не ожидаю, что от этого что-то произойдет: вы предлагаете добавить специальное исключение к правилам языка, которое потенциально изменяет поведение существующего кода, и не даете никаких убедительных вариантов его использования. Какую выгоду вы видите в
P < Q
over!P amp;amp; Q
?
Ответ №2:
На самом деле я думаю, что это может ускорить код. Вот код для первой функции:
bool biconditional(bool a, bool b)
{
return (a amp;amp; b) || (!a amp;amp; !b);
}
bool biconditional_trick(bool a, bool b)
{
return a == b;
}
И сгенерированная сборка:
biconditional(bool, bool):
mov eax, esi
xor eax, 1
xor eax, edi
ret
biconditional_trick(bool, bool):
cmp dil, sil
sete al
ret
Я использовал gcc 5.3 из Compiler Explorer с флагами -O3 -Wall -fno-verbose-asm -march=haswell
.
Очевидно, что вы можете срезать 1 инструкцию. Интересно, что gcc не выполняет эту оптимизацию. Возможно, вы захотите отправить им электронное письмо и спросить, почему: https://gcc.gnu.org/lists.html
Редактировать: другой ответ имеет смысл: логические выражения можно вычислять быстрее, обрезая ненужные части. Чтобы продемонстрировать, я переписал код, чтобы использовать вызовы функций, которые возвращают bool
вместо bool
аргументов:
bool fa();
bool fb();
bool biconditional_with_function()
{
return (fa() amp;amp; fb()) || (!fa() amp;amp; !fb());
}
bool biconditional_with_function_trick()
{
return fa() == fb();
}
Вот сборка:
biconditional_with_function():
sub rsp, 8
call fa()
test al, al
je .L7
call fb()
test al, al
jne .L10
.L7:
call fa()
mov edx, eax
xor eax, eax
test dl, dl
je .L14
.L10:
add rsp, 8
ret
.L14:
call fb()
add rsp, 8
xor eax, 1
ret
biconditional_with_function_trick():
push rbx
call fa()
mov ebx, eax
call fb()
cmp bl, al
pop rbx
sete al
ret
Вы можете видеть, что код, сгенерированный для biconditional_with_function
uses, переходит, чтобы пропустить вторую половину выражения, если первая половина истинна. Интересно, что когда вторая половина вычисляется fa()
и fb()
вызывается в целом дважды, поскольку компилятор не знает, всегда ли они возвращают один и тот же результат. Если это так, код следует переписать, сохранив оцененные результаты в их собственных переменных:
bool biconditional_with_function_rewritten()
{
bool a = fa();
bool b = fb();
return (a amp;amp; b) || (!a amp;amp; !b);
}
И сборка:
biconditional_with_function_rewritten():
push rbx
call fa()
mov ebx, eax
call fb()
xor eax, 1
xor eax, ebx
pop rbx
ret
Мы видим, что они почти идентичны, остается только разница в 1 инструкции, что дает методу «трюк» небольшое преимущество.
Для обратного неимпликации мы можем видеть, что действительно GCC избегает вычисления второго оператора при использовании логических операторов, но не при использовании <
оператора:
bool fa();
bool fb();
bool converse_nonimplication()
{
return !fa() amp;amp; fb();
}
bool converse_nonimplication_trick()
{
return fa() < fb();
}
Сборка:
converse_nonimplication():
sub rsp, 8
call fa()
test al, al
je .L5
xor eax, eax
add rsp, 8
ret
.L5:
add rsp, 8
jmp fb()
converse_nonimplication_trick():
push rbx
call fa()
mov ebx, eax
call fb()
cmp al, bl
pop rbx
seta al
ret
Комментарии:
1. Хм, действительно. В первом примере есть старый отчет об ошибке с пропущенной оптимизацией для GCC, хотя код, который генерируется сейчас, уже намного лучше, чем тогда. Это не так просто, как «чем меньше инструкций, тем быстрее», поэтому, возможно, стоит проверить фактические затраты на инструкции для обеих версий, но если они не работают одинаково быстро, в одной или другой есть пропущенная оптимизация.