哪个值更好用?布尔真还是整数 1?
上述主题让我在 if
条件下对 bool
和 int
做了一些实验。所以只是出于好奇,我写了这个程序:
int f(int i)
{
if ( i ) return 99; //if(int)
else return -99;
}
int g(bool b)
{
if ( b ) return 99; //if(bool)
else return -99;
}
int main(){}
g++ intbool.cpp -S
为每个函数生成 asm 代码,如下所示:
f(int) __Z1fi 的 asm 代码:LFB0: pushl %ebp LCFI0: movl %esp, %ebp LCFI1: cmpl $0, 8(%ebp) je L2 movl $99, %eax jmp L3 L2: movl $-99, %eax L3:离开 LCFI2:ret
g(bool) __Z1gb 的 asm 代码:LFB1: pushl %ebp LCFI3: movl %esp, %ebp LCFI4: subl $4, %esp LCFI5: movl 8(%ebp), %eax movb %al, -4(%ebp) cmpb $0, -4(%ebp) je L5 movl $99, %eax jmp L6 L5: movl $-99, %eax L6: 离开 LCFI6: ret
令人惊讶的是,g(bool)
生成了更多 asm
指令!这是否意味着 if(bool)
比 if(int)
慢一点?我以前认为 bool
是专门设计用于 if
等条件语句的,所以我期望 g(bool)
生成更少的 asm 指令,从而使 g(bool)
更高效、更快。
编辑:
到目前为止,我没有使用任何优化标志。但即使没有它,为什么它会为 g(bool)
生成更多的 asm 是我正在寻找一个合理答案的问题。我还应该告诉您,-O2
优化标志生成完全相同的 asm。但这不是问题。问题是我问过的。
g(bool)
生成更多的 asm 是我正在寻找一个合理答案的问题。
我感觉合理。您的编译器显然将 bool
定义为 8 位值,并且您的系统 ABI 要求它在将小(< 32 位)整数参数推入调用堆栈时将其“提升”为 32 位。因此,为了比较 bool
,编译器生成代码以隔离 g 接收的 32 位参数的最低有效字节,并将其与 cmpb
进行比较。在第一个示例中,int
参数使用了全部压入堆栈的 32 位,因此它只是将整个内容与 cmpl
进行比较。
使用 -03
编译为我提供了以下内容:
F:
pushl %ebp
movl %esp, %ebp
cmpl $1, 8(%ebp)
popl %ebp
sbbl %eax, %eax
andb $58, %al
addl $99, %eax
ret
G:
pushl %ebp
movl %esp, %ebp
cmpb $1, 8(%ebp)
popl %ebp
sbbl %eax, %eax
andb $58, %al
addl $99, %eax
ret
.. 所以它编译成基本相同的代码,除了 cmpl
和 cmpb
。这意味着差异(如果有的话)并不重要。以未优化的代码来判断是不公平的。
编辑以澄清我的观点。未优化的代码是为了简单的调试,而不是为了速度。比较未优化代码的速度是没有意义的。
cmpl
表示一个而 cmpb
表示另一个?
bool
是一个字节,而 int
是四个。我认为没有什么比这更特别的了。
bool
视为 8 位类型。
char
,按定义是一个字节,是最小的可寻址单元。 bool
的大小是实现定义的,可以是 1、4 或 8,或其他任何值。不过,编译器倾向于将其合二为一。
当我用一组健全的选项(特别是-O3)编译它时,我得到了:
对于 f()
:
.type _Z1fi, @function
_Z1fi:
.LFB0:
.cfi_startproc
.cfi_personality 0x3,__gxx_personality_v0
cmpl $1, %edi
sbbl %eax, %eax
andb $58, %al
addl $99, %eax
ret
.cfi_endproc
对于 g()
:
.type _Z1gb, @function
_Z1gb:
.LFB1:
.cfi_startproc
.cfi_personality 0x3,__gxx_personality_v0
cmpb $1, %dil
sbbl %eax, %eax
andb $58, %al
addl $99, %eax
ret
.cfi_endproc
他们仍然使用不同的指令进行比较(cmpb
用于布尔值与 cmpl
用于 int),但除此之外,主体是相同的。快速浏览一下英特尔手册会告诉我:……没什么。英特尔手册中没有 cmpb
或 cmpl
这样的东西。他们都是cmp
,我现在找不到时间表。但是,我猜想比较字节立即数与比较长立即数之间没有时钟差异,因此出于所有实际目的,代码是相同的。
编辑以根据您的添加添加以下内容
在未优化的情况下代码不同的原因是它未优化。 (是的,它是循环的,我知道。)当编译器遍历 AST 并直接生成代码时,除了它所在的 AST 的直接点是什么之外,它不“知道”任何东西。此时它缺乏所需的所有上下文信息要知道在这个特定点它可以将声明的类型 bool
视为 int
。默认情况下,布尔值显然被视为一个字节,并且在英特尔世界中处理字节时,您必须执行符号扩展以使其达到特定宽度以将其放入堆栈等操作。(您不能推送一个字节.)
然而,当优化器查看 AST 并发挥其魔力时,它会查看周围的上下文并“知道”何时可以在不改变语义的情况下用更有效的东西替换代码。所以它“知道”它可以在参数中使用整数,从而失去不必要的转换和扩大。
l
和 b
是仅在 AT&T 语法中使用的后缀。它们只是分别使用 4 字节(长)和 1 字节(字节)操作数来引用 cmp
的版本。如果 intel 语法有任何歧义,通常会用 BYTE PTR
、WORD PTR
或 DWORD PTR
标记内存操作数,而不是在操作码上添加后缀。
cmp
的两个操作数大小具有相同的性能,并且对于 读取 %dil
没有部分寄存器惩罚。 (但这并不能阻止 clang 通过在 AL 上使用字节大小 and
作为 99 和 -99 之间的无分支大小写翻转的一部分来有趣地创建部分寄存器停顿。)
至少在 Linux 和 Windows 上使用 GCC 4.5,sizeof(bool) == 1
。在 x86 和 x86_64 上,您不能将低于通用寄存器值的值传递给函数(无论是通过堆栈还是寄存器,取决于调用约定等......)。
所以 bool 的代码,当未优化时,实际上会花费一定的长度从参数堆栈中提取该 bool 值(使用另一个堆栈槽来保存该字节)。它比仅仅提取一个本地寄存器大小的变量更复杂。
sizeof(bool)
和 sizeof(wchar_t)
是实现定义的。”所以说 sizeof(bool) == 1
并不完全正确,除非你在说话关于特定编译器的特定版本。
是的,讨论很有趣。但只需测试它:
测试代码:
#include <stdio.h>
#include <string.h>
int testi(int);
int testb(bool);
int main (int argc, char* argv[]){
bool valb;
int vali;
int loops;
if( argc < 2 ){
return 2;
}
valb = (0 != (strcmp(argv[1], "0")));
vali = strcmp(argv[1], "0");
printf("Arg1: %s\n", argv[1]);
printf("BArg1: %i\n", valb ? 1 : 0);
printf("IArg1: %i\n", vali);
for(loops=30000000; loops>0; loops--){
//printf("%i: %i\n", loops, testb(valb=!valb));
printf("%i: %i\n", loops, testi(vali=!vali));
}
return valb;
}
int testi(int val){
if( val ){
return 1;
}
return 0;
}
int testb(bool val){
if( val ){
return 1;
}
return 0;
}
在 64 位 Ubuntu 10.10 笔记本电脑上编译:g++ -O3 -o /tmp/test_i /tmp/test_i.cpp
基于整数的比较:
sauer@trogdor:/tmp$ time /tmp/test_i 1 > /dev/null
real 0m8.203s
user 0m8.170s
sys 0m0.010s
sauer@trogdor:/tmp$ time /tmp/test_i 1 > /dev/null
real 0m8.056s
user 0m8.020s
sys 0m0.000s
sauer@trogdor:/tmp$ time /tmp/test_i 1 > /dev/null
real 0m8.116s
user 0m8.100s
sys 0m0.000s
布尔测试/打印未注释(和整数注释):
sauer@trogdor:/tmp$ time /tmp/test_i 1 > /dev/null
real 0m8.254s
user 0m8.240s
sys 0m0.000s
sauer@trogdor:/tmp$ time /tmp/test_i 1 > /dev/null
real 0m8.028s
user 0m8.000s
sys 0m0.010s
sauer@trogdor:/tmp$ time /tmp/test_i 1 > /dev/null
real 0m7.981s
user 0m7.900s
sys 0m0.050s
它们与 1 个分配和 2 个比较相同,每个循环超过 3000 万个循环。找到其他要优化的东西。例如,不要不必要地使用 strcmp。 ;)
在机器级别没有 bool 这样的东西
很少有指令集架构定义任何类型的布尔操作数类型,尽管经常有指令触发对非零值的操作。对于 CPU,通常,一切都是标量类型之一或它们的字符串。
给定的编译器和给定的 ABI 将需要为 int
和 bool
选择特定的大小,当像您的情况一样,这些大小不同时,它们可能会生成略有不同的代码,并且在某些优化级别上可能会略有不同快点。
为什么 bool 在许多系统上都是一个字节?
为 bool 选择 char
类型更安全,因为有人可能会创建一个非常大的数组。
更新:“更安全”,我的意思是:对于编译器和库实现者。我并不是说人们需要重新实现系统类型。
bool
由位表示,x86 的开销;所以在许多实现中,字节将是速度/数据紧凑性的一个很好的折衷。
char
而不是 bool
”,而是在指代编译器为 bool
对象选择的大小时简单地使用“char
类型”来表示“1 个字节” .
它主要取决于编译器和优化。这里有一个有趣的讨论(与语言无关):
Does "if ([bool] == true)" require one more step than "if ([bool])"?
以两种不同的方式处理您的问题:
如果您专门谈论 C++ 或任何会为此生成汇编代码的编程语言,我们将受限于编译器将在 ASM 中生成的代码。我们也受制于 c++ 中真假的表示。整数必须以 32 位存储,我可以简单地使用一个字节来存储布尔表达式。条件语句的 Asm 片段:
对于整数:
mov eax,dword ptr[esp] ;Store integer
cmp eax,0 ;Compare to 0
je false ;If int is 0, its false
;Do what has to be done when true
false:
;Do what has to be done when false
对于布尔:
mov al,1 ;Anything that is not 0 is true
test al,1 ;See if first bit is fliped
jz false ;Not fliped, so it's false
;Do what has to be done when true
false:
;Do what has to be done when false
所以,这就是速度比较如此依赖于编译的原因。在上述情况下,布尔值会稍微快一些,因为 cmp
意味着设置标志的减法。它也与您的编译器生成的内容相矛盾。
另一种方法,一种更简单的方法,是自己查看表达式的逻辑,尽量不要担心编译器将如何翻译你的代码,我认为这是一种更健康的思维方式。归根结底,我仍然相信编译器生成的代码实际上是在试图给出一个真实的解决方案。我的意思是,也许如果您在 if 语句中增加测试用例并在一侧坚持使用布尔值,而在另一侧坚持使用整数,编译器会这样做,因此生成的代码将在机器级别使用布尔表达式更快地执行。
我认为这是一个概念性问题,所以我将给出一个概念性的答案。这个讨论让我想起了我经常讨论的关于代码效率是否会转化为更少的汇编代码行数的讨论。似乎这个概念被普遍接受为正确的。考虑到跟踪 ALU 处理每条语句的速度是不可行的,第二种选择是专注于汇编中的跳转和比较。在这种情况下,您提供的代码中布尔语句或整数之间的区别变得相当具有代表性。 C++ 中表达式的结果将返回一个值,该值将被赋予一个表示。另一方面,在汇编中,跳转和比较将基于数值,而不管在 C++ if 语句中评估的是什么类型的表达式。在这些问题上重要的是要记住,诸如此类的纯逻辑语句最终会产生巨大的计算开销,即使单个位也能完成同样的事情。
不定期副业成功案例分享
__int64
比int
快?还是 CPU 分别处理 32 位整数和 32 位指令集?bool
强制转换到堆栈,而不是一次性将 32 位单元推入堆栈,不需要强制转换,这就是你的意思吗?在这个演员阵容中使用int
更快吗?