ChatGPT解决这个技术问题 Extra ChatGPT

哪个更快:if (bool) 或 if(int)?

哪个值更好用?布尔真还是整数 1?

上述主题让我在 if 条件下对 boolint 做了一些实验。所以只是出于好奇,我写了这个程序:

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。但这不是问题。问题是我问过的。

这也是一个不公平的测试,除非您将它们与启用的合理优化进行比较。
@Daniel:我没有使用任何优化标志。但即使没有它,为什么它会为 g(bool) 生成更多的 asm 是我正在寻找一个合理答案的问题。
你为什么要费心去阅读汇编,而不是仅仅运行程序和计时结果呢?指令的数量并没有真正说明性能。您不仅需要考虑指令长度,还需要考虑依赖关系和指令类型(其中一些是使用较慢的微码路径解码的,它们需要哪些执行单元,指令的延迟和吞吐量是多少,是分支?内存访问?
@user 未知,@Malvolio:很明显;我不是为生产代码做所有这些。正如我在文章开头已经提到的那样,“所以出于好奇,我编写了这个程序”。所以,是的,它纯粹是假设性的。
这是一个合理的问题。它们要么相等,要么更快。 ASM 的发布可能是为了提供帮助或大声思考,因此与其将其用作回避问题并说“只需编写可读代码”的方式,不如直接回答问题或 STFU,如果您不知道或没有什么有用的东西要说;)我的贡献是这个问题是可以回答的,“只写可读的代码”只不过是回避这个问题。

S
Sherm Pendley

我感觉合理。您的编译器显然将 bool 定义为 8 位值,并且您的系统 ABI 要求它在将小(< 32 位)整数参数推入调用堆栈时将其“提升”为 32 位。因此,为了比较 bool,编译器生成代码以隔离 g 接收的 32 位参数的最低有效字节,并将其与 cmpb 进行比较。在第一个示例中,int 参数使用了全部压入堆栈的 32 位,因此它只是将整个内容与 cmpl 进行比较。


我同意。这有助于说明在选择变量类型时,您选择它是为了两个可能相互竞争的目的,即存储空间与计算性能。
这是否也适用于 64 位进程,即 __int64int 快?还是 CPU 分别处理 32 位整数和 32 位指令集?
@CrendKing 也许值得提出另一个问题?
我希望您在回答中提供更多信息;所以你的意思是编译器会执行更多指令来将一个字节 bool 强制转换到堆栈,而不是一次性将 32 位单元推入堆栈,不需要强制转换,这就是你的意思吗?在这个演员阵容中使用 int 更快吗?
g
gsamaras

使用 -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

.. 所以它编译成基本相同的代码,除了 cmplcmpb。这意味着差异(如果有的话)并不重要。以未优化的代码来判断是不公平的。

编辑以澄清我的观点。未优化的代码是为了简单的调试,而不是为了速度。比较未优化代码的速度是没有意义的。


尽管我同意您的结论,但我认为您正在跳过有趣的部分。 为什么它使用 cmpl 表示一个而 cmpb 表示另一个?
@jalf:因为 bool 是一个字节,而 int 是四个。我认为没有什么比这更特别的了。
我认为其他回复更关注原因:这是因为有问题的平台将 bool 视为 8 位类型。
@Nathan:不。C++ 没有位数据类型。最小的类型是char,按定义是一个字节,是最小的可寻址单元。 bool 的大小是实现定义的,可以是 1、4 或 8,或其他任何值。不过,编译器倾向于将其合二为一。
@Nathan:这在 Java 中也很棘手。 Java 表示布尔值表示的数据是一位的值,但如何存储该位仍然是实现定义的。实用计算机根本不处理位。
J
JUST MY correct OPINION

当我用一组健全的选项(特别是-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),但除此之外,主体是相同的。快速浏览一下英特尔手册会告诉我:……没什么。英特尔手册中没有 cmpbcmpl 这样的东西。他们都是cmp,我现在找不到时间表。但是,我猜想比较字节立即数与比较长立即数之间没有时钟差异,因此出于所有实际目的,代码是相同的。

编辑以根据您的添加添加以下内容

在未优化的情况下代码不同的原因是它未优化。 (是的,它是循环的,我知道。)当编译器遍历 AST 并直接生成代码时,除了它所在的 AST 的直接点是什么之外,它不“知道”任何东西。此时它缺乏所需的所有上下文信息要知道在这个特定点它可以将声明的类型 bool 视为 int。默认情况下,布尔值显然被视为一个字节,并且在英特尔世界中处理字节时,您必须执行符号扩展以使其达到特定宽度以将其放入堆栈等操作。(您不能推送一个字节.)

然而,当优化器查看 AST 并发挥其魔力时,它会查看周围的上下文并“知道”何时可以在不改变语义的情况下用更有效的东西替换代码。所以它“知道”它可以在参数中使用整数,从而失去不必要的转换和扩大。


哈哈,我喜欢编译器如何简单地返回 99,或者 99+58 = 157 = -99(有符号 8 位溢出)......很有趣。
@Daniel:即使我也喜欢。起初,我说“-99 在哪里”,然后我立即意识到它在做一些非常古怪的事情。
lb 是仅在 AT&T 语法中使用的后缀。它们只是分别使用 4 字节(长)和 1 字节(字节)操作数来引用 cmp 的版本。如果 intel 语法有任何歧义,通常会用 BYTE PTRWORD PTRDWORD PTR 标记内存操作数,而不是在操作码上添加后缀。
时序表:agner.org/optimize cmp 的两个操作数大小具有相同的性能,并且对于 读取 %dil 没有部分寄存器惩罚。 (但这并不能阻止 clang 通过在 AL 上使用字节大小 and 作为 99 和 -99 之间的无分支大小写翻转的一部分来有趣地创建部分寄存器停顿。)
M
Mat

至少在 Linux 和 Windows 上使用 GCC 4.5,sizeof(bool) == 1。在 x86 和 x86_64 上,您不能将低于通用寄存器值的值传递给函数(无论是通过堆栈还是寄存器,取决于调用约定等......)。

所以 bool 的代码,当未优化时,实际上会花费一定的长度从参数堆栈中提取该 bool 值(使用另一个堆栈槽来保存该字节)。它比仅仅提取一个本地寄存器大小的变量更复杂。


根据 C++03 标准,§5.3.3/1:“sizeof(bool)sizeof(wchar_t) 是实现定义的。”所以说 sizeof(bool) == 1 并不完全正确,除非你在说话关于特定编译器的特定版本。
d
dannysauer

是的,讨论很有趣。但只需测试它:

测试代码:

#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。 ;)


D
DigitalRoss

在机器级别没有 bool 这样的东西

很少有指令集架构定义任何类型的布尔操作数类型,尽管经常有指令触发对非零值的操作。对于 CPU,通常,一切都是标量类型之一或它们的字符串。

给定的编译器和给定的 ABI 将需要为 intbool 选择特定的大小,当像您的情况一样,这些大小不同时,它们可能会生成略有不同的代码,并且在某些优化级别上可能会略有不同快点。

为什么 bool 在许多系统上都是一个字节?

为 bool 选择 char 类型更安全,因为有人可能会创建一个非常大的数组。

更新:“更安全”,我的意思是:对于编译器和库实现者。我并不是说人们需要重新实现系统类型。


+1 想象一下如果 bool 由位表示,x86 的开销;所以在许多实现中,字节将是速度/数据紧凑性的一个很好的折衷。
@Billy:我认为他不是在说“使用 char 而不是 bool”,而是在指代编译器为 bool 对象选择的大小时简单地使用“char 类型”来表示“1 个字节” .
哦,当然,我并不是说每个程序都应该选择,我只是在提出系统布尔类型为 1 字节的理由。
@Dennis:啊,这是有道理的。
C
Community

它主要取决于编译器和优化。这里有一个有趣的讨论(与语言无关):

Does "if ([bool] == true)" require one more step than "if ([bool])"?

另外,看看这篇文章:http://www.linuxquestions.org/questions/programming-9/c-compiler-handling-of-boolean-variables-290996/


A
Artie

以两种不同的方式处理您的问题:

如果您专门谈论 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 语句中评估的是什么类型的表达式。在这些问题上重要的是要记住,诸如此类的纯逻辑语句最终会产生巨大的计算开销,即使单个位也能完成同样的事情。