布尔技巧的比较运算符
Comparison operators on booleans trick
在C++中,逻辑运算符&&
、||
、!
都有,分别对应合取、析取、取反,分别.
但我注意到比较运算符==
、!=
、<
、>
、<=
、>=
可用于布尔值也是如此!鉴于 P
和 Q
是布尔值:
P == Q
是双条件 ,
P != Q
是排他性析取 ,
P < Q
是逆向非蕴涵 ,
P > Q
是非蕴涵 ,
P <= Q
是蕴涵,
而P >= Q
是逆蕴涵.
我的问题是:
程序会通过这个技巧执行得更好吗?
是否有任何使用此技巧的示例代码(任何语言)?
Will the performance increase by this trick?
在任何有好处的处理器上,当 P
和 Q
是简单变量时,这将足够简单,您应该期望编译器已经使用它而不需要任何源代码重写。
但请记住,P < Q
通常比 !P && Q
有明显的 缺点 :它需要评估 Q
,当如果 P
的计算结果为 true
,则结果已知。这同样适用于所有其他关系运算符。
Is there any example code using this trick (in any language)?
不是把戏,而是因为它可以使代码更容易理解(不是任何特定语言):
if ((a == null) != (b == null))
throw "a and b must either both be null, or both be non-null";
可以写成^
。哪个更容易阅读,见仁见智。
实际上我认为它可能会使代码更快。这是第一个函数的代码:
bool biconditional(bool a, bool b)
{
return (a && b) || (!a && !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
我使用了来自 Compiler Explorer 的 gcc 5.3 和标志 -O3 -Wall -fno-verbose-asm -march=haswell
。
很明显,您可以减少 1 条指令。有趣的是 gcc 没有做这个优化。您可能想给他们发一封电子邮件并询问原因:https://gcc.gnu.org/lists.html
编辑:另一个答案提出了一个很好的观点:通过修剪不必要的部分可以更快地评估逻辑表达式。为了演示,我重写了代码以使用对 return bool
而不是 bool
参数的函数的调用:
bool fa();
bool fb();
bool biconditional_with_function()
{
return (fa() && fb()) || (!fa() && !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
生成的代码使用跳转跳过表达式的后半部分。有趣的是,当计算后半部分时,fa()
和 fb()
被调用两次,因为编译器不知道它们是否总是 return 相同的结果。如果是这种情况,则应通过将评估结果保存在自己的变量中来重写代码:
bool biconditional_with_function_rewritten()
{
bool a = fa();
bool b = fb();
return (a && b) || (!a && !b);
}
和程序集:
biconditional_with_function_rewritten():
push rbx
call fa()
mov ebx, eax
call fb()
xor eax, 1
xor eax, ebx
pop rbx
ret
我们可以看到它们几乎相同,只剩下 1 个指令差异,使 "trick" 方法略有优势。
对于相反的非蕴涵,我们可以看到 GCC 在使用逻辑运算符时确实会避免计算第二个运算符,但在使用 <
运算符时不会:
bool fa();
bool fb();
bool converse_nonimplication()
{
return !fa() && 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
在C++中,逻辑运算符&&
、||
、!
都有,分别对应合取
但我注意到比较运算符==
、!=
、<
、>
、<=
、>=
可用于布尔值也是如此!鉴于 P
和 Q
是布尔值:
P == Q
是双条件
P != Q
是排他性析取
P < Q
是逆向非蕴涵
P > Q
是非蕴涵
P <= Q
是蕴涵
而P >= Q
是逆蕴涵
我的问题是:
程序会通过这个技巧执行得更好吗?
是否有任何使用此技巧的示例代码(任何语言)?
Will the performance increase by this trick?
在任何有好处的处理器上,当 P
和 Q
是简单变量时,这将足够简单,您应该期望编译器已经使用它而不需要任何源代码重写。
但请记住,P < Q
通常比 !P && Q
有明显的 缺点 :它需要评估 Q
,当如果 P
的计算结果为 true
,则结果已知。这同样适用于所有其他关系运算符。
Is there any example code using this trick (in any language)?
不是把戏,而是因为它可以使代码更容易理解(不是任何特定语言):
if ((a == null) != (b == null))
throw "a and b must either both be null, or both be non-null";
可以写成^
。哪个更容易阅读,见仁见智。
实际上我认为它可能会使代码更快。这是第一个函数的代码:
bool biconditional(bool a, bool b)
{
return (a && b) || (!a && !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
我使用了来自 Compiler Explorer 的 gcc 5.3 和标志 -O3 -Wall -fno-verbose-asm -march=haswell
。
很明显,您可以减少 1 条指令。有趣的是 gcc 没有做这个优化。您可能想给他们发一封电子邮件并询问原因:https://gcc.gnu.org/lists.html
编辑:另一个答案提出了一个很好的观点:通过修剪不必要的部分可以更快地评估逻辑表达式。为了演示,我重写了代码以使用对 return bool
而不是 bool
参数的函数的调用:
bool fa();
bool fb();
bool biconditional_with_function()
{
return (fa() && fb()) || (!fa() && !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
生成的代码使用跳转跳过表达式的后半部分。有趣的是,当计算后半部分时,fa()
和 fb()
被调用两次,因为编译器不知道它们是否总是 return 相同的结果。如果是这种情况,则应通过将评估结果保存在自己的变量中来重写代码:
bool biconditional_with_function_rewritten()
{
bool a = fa();
bool b = fb();
return (a && b) || (!a && !b);
}
和程序集:
biconditional_with_function_rewritten():
push rbx
call fa()
mov ebx, eax
call fb()
xor eax, 1
xor eax, ebx
pop rbx
ret
我们可以看到它们几乎相同,只剩下 1 个指令差异,使 "trick" 方法略有优势。
对于相反的非蕴涵,我们可以看到 GCC 在使用逻辑运算符时确实会避免计算第二个运算符,但在使用 <
运算符时不会:
bool fa();
bool fb();
bool converse_nonimplication()
{
return !fa() && 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