哪个更快:while(1)或while(2)?

这是一位高级经理提出的面试问题。

哪个更快?

while(1) {
    // Some code
}

要么

while(2) {
    //Some code
}

我说,都具有相同的执行速度,因为里面的表现while应该最终评估为truefalse 。 在这种情况下,两者都评估为true并且在while条件内没有额外的条件指令。 所以,两者将具有相同的执行速度,我更喜欢while(1)。

但面试官自信地说:“检查你的基本知识, while(1)while(2)快。” (他没有测试我的信心)

这是真的?

另见:“for(;;)”比“while(TRUE)”更快吗? 如果不是,人们为什么使用它?


两个循环都是无限的,但是我们可以看到哪个循环每次迭代需要更多指令/资源。

使用gcc,我将以下两个程序编译为不同级别的优化程序:

int main(void) {
    while(1) {}
    return 0;
}


int main(void) {
    while(2) {}
    return 0;
}

即使没有优化( -O0 ), 生成的程序集也是相同的。 因此,两个环路之间没有速度差异。

作为参考,这里是生成的程序集(使用gcc main.c -S -masm=intel带有优化标志):

-O0

    .file   "main.c"
    .intel_syntax noprefix
    .def    __main; .scl    2;  .type   32; .endef
    .text
    .globl  main
    .def    main;   .scl    2;  .type   32; .endef
    .seh_proc   main
main:
    push    rbp
    .seh_pushreg    rbp
    mov rbp, rsp
    .seh_setframe   rbp, 0
    sub rsp, 32
    .seh_stackalloc 32
    .seh_endprologue
    call    __main
.L2:
    jmp .L2
    .seh_endproc
    .ident  "GCC: (tdm64-2) 4.8.1"

-O1

    .file   "main.c"
    .intel_syntax noprefix
    .def    __main; .scl    2;  .type   32; .endef
    .text
    .globl  main
    .def    main;   .scl    2;  .type   32; .endef
    .seh_proc   main
main:
    sub rsp, 40
    .seh_stackalloc 40
    .seh_endprologue
    call    __main
.L2:
    jmp .L2
    .seh_endproc
    .ident  "GCC: (tdm64-2) 4.8.1"

使用-O2-O3 (相同的输出):

    .file   "main.c"
    .intel_syntax noprefix
    .def    __main; .scl    2;  .type   32; .endef
    .section    .text.startup,"x"
    .p2align 4,,15
    .globl  main
    .def    main;   .scl    2;  .type   32; .endef
    .seh_proc   main
main:
    sub rsp, 40
    .seh_stackalloc 40
    .seh_endprologue
    call    __main
.L2:
    jmp .L2
    .seh_endproc
    .ident  "GCC: (tdm64-2) 4.8.1"

事实上,为循环生成的程序集对于每个优化级别都是相同的:

 .L2:
    jmp .L2
    .seh_endproc
    .ident  "GCC: (tdm64-2) 4.8.1"

重要的是:

.L2:
    jmp .L2

我无法很好地阅读程序集,但显然这是一个无条件的循环。 jmp指令无条件地将程序重新设置回.L2标签,甚至不需要将值与真实值进行比较,当然,直到程序以某种方式结束后才立即执行。 这直接对应于C / C ++代码:

L2:
    goto L2;

编辑:

有趣的是,即使没有优化,下面的循环都会在汇编中产生完全相同的输出(无条件jmp ):

while(42) {}

while(1==1) {}

while(2==2) {}

while(4<7) {}

while(3==3 && 4==4) {}

while(8-9 < 0) {}

while(4.3 * 3e4 >= 2 << 6) {}

while(-0.1 + 02) {}

甚至令我惊讶的是:

#include<math.h>

while(sqrt(7)) {}

while(hypot(3,4)) {}

用户定义的函数让事情变得更加有趣:

int x(void) {
    return 1;
}

while(x()) {}


#include<math.h>

double x(void) {
    return sqrt(7);
}

while(x()) {}

-O0 ,这两个示例实际上调用x并为每次迭代执行比较。

第一个例子(返回1):

.L4:
    call    x
    testl   %eax, %eax
    jne .L4
    movl    $0, %eax
    addq    $32, %rsp
    popq    %rbp
    ret
    .seh_endproc
    .ident  "GCC: (tdm64-2) 4.8.1"

第二个例子(返回sqrt(7) ):

.L4:
    call    x
    xorpd   %xmm1, %xmm1
    ucomisd %xmm1, %xmm0
    jp  .L4
    xorpd   %xmm1, %xmm1
    ucomisd %xmm1, %xmm0
    jne .L4
    movl    $0, %eax
    addq    $32, %rsp
    popq    %rbp
    ret
    .seh_endproc
    .ident  "GCC: (tdm64-2) 4.8.1"

但是,在-O1及更高版本中,它们都产生与前面示例相同的程序集(无条件jmp返回到前面的标签)。

TL; DR

在GCC下,不同的循环被编译成相同的程序集。 编译器评估常量值,并且不会执行任何实际比较。

故事的寓意是:

  • C ++源代码和CPU指令之间存在一层转换,这一层对性能有重要影响。
  • 因此,仅通过查看源代码无法评估性能。
  • 编译器应该足够聪明以优化这种微不足道的情况。 程序员不应该在绝大多数情况下浪费时间来思考它们。

  • 是的, while(1)while(2)快得多,但为了让人们阅读! 如果我while(1)不熟悉的代码库中看到,我立即知道作者的意图,我的眼球可以继续下一行。

    如果我while(2)看到,我可能会停下脚步,试图弄清楚作者为什么不写while(1) 。 作者的手指滑落在键盘上了吗? 这个代码库的维护者使用while(n)作为一个难以理解的评论机制来使循环看起来不同? 对于一些破损的静态分析工具中的虚假警告,这是一个粗略的解决方法吗? 或者这是我正在阅读生成的代码的线索? 这是一个由不明智的查找和替换 - 全部,或不合并或宇宙射线造成的错误吗? 也许这行代码应该做一些完全不同的事情。 也许它应该while(w)while(x2)读取。 我最好在文件的历史中找到作者,并向他们发送“WTF”电子邮件......现在我打破了我的心理背景。 while(2)可能会消耗我几分钟的时间, while(1)会花费几分之一秒!

    我夸大,但只是一点点。 代码可读性非常重要。 这在采访中值得一提!


    显示由特定编译器为特定目标生成的特定代码集的代码的现有答案并不能完全回答这个问题 - 除非在特定的上下文中提出了问题(“使用gcc 4.7.2 for x86_64更快与默认选项?“,例如)。

    就语言定义而言,抽象机中while (1)评估整数常量1 ,而while (2)评估整数常数2 ; 在这两种情况下,结果都相等比较为零。 语言标准完全没有提到这两种结构的相对表现。

    我可以想象,一个非常天真的编译器可能为这两种形式生成不同的机器代码,至少在编译时没有请求优化。

    另一方面,C编译器绝对必须在编译时评估一些常量表达式,当它们出现在需要常量表达式的上下文中时。 例如,这个:

    int n = 4;
    switch (n) {
        case 2+2: break;
        case 4:   break;
    }
    

    需要诊断; 一个懒惰的编译器没有延迟评估2+2的选项,直到执行时间。 由于编译器必须能够在编译时评估常量表达式,因此即使不需要,也没有充分利用该功能的好处。

    C标准(N1570 6.8.5p4)说

    迭代语句会导致一个称为循环体的语句被重复执行,直到控制表达式比较等于0。

    所以相关的常量表达式是1 == 02 == 0 ,它们都是int0 。 (这些比较隐含在while循环的语义中;它们不作为实际的C表达式存在。)

    一个反过来朴素的编译器可能会为这两个构造生成不同的代码。 例如,对于第一个,它可以生成一个无条件的无限循环(将1视为特殊情况),第二个循环可以生成等价于2 != 0的显式运行时比较。 但是我从来没有遇到过实际上会这样做的C编译器,并且我非常怀疑这样的编译器是否存在。

    大多数编译器(我很想说所有的产品质量编译器)都有选择来请求额外的优化。 在这种选择下,任何编译器都不可能为这两种表单生成不同的代码。

    如果您的编译器为这两个构造生成不同的代码,请首先检查不同的代码序列是否实际具有不同的性能。 如果他们这样做,请尝试再次使用优化选项编译(如果可用)。 如果它们仍然不同,请向编译器供应商提交错误报告。 它不是(不一定)是一个错误,不符合C标准,但它几乎肯定是一个应该纠正的问题。

    底线: while (1)while(2)几乎肯定有相同的表现。 它们具有完全相同的语义,并且没有任何编译器不会生成相同的代码。

    尽管编译器为while(1)while(2)更快的代码是完全合法的,但编译器为while(1)产生更快的代码比在while(1)中产生另一个while(1)代码同样合法。同样的节目。

    (你所问的问题还隐含着另一个问题:你如何处理坚持不正确的技术点的面试官,这对Workplace网站可能是个好问题)。

    链接地址: http://www.djcxy.com/p/2231.html

    上一篇: Which is faster: while(1) or while(2)?

    下一篇: OpenMP: poor performance of heap arrays (stack arrays work fine)