这是一位高级经理在面试时问的问题。

哪个更快?

while(1) {
    // Some code
}

or

while(2) {
    //Some code
}

我说过两者具有相同的执行速度,因为while中的表达式最终应求值为true或false。在这种情况下,两者都求值为true,并且while条件中没有额外的条件指令。因此,两者将具有相同的执行速度,我更喜欢while(1)。

但面试官自信地说: “检查你的基本知识。While(1)比While(2)快。” (他不是在试探我的信心)

这是真的吗?

请参见:“for(;;)”是否比“while (TRUE)”快?如果不是,人们为什么要使用它?


当前回答

我曾经用C语言和汇编代码编写程序,那时候这类废话可能会有很大不同。当它确实起作用时,我们在汇编中写了出来。

如果我被问到这个问题,我会重复唐纳德·克努特1974年关于过早优化的名言,如果面试官没有笑,我就会走开,然后继续前进。

其他回答

我能想到为什么while(2)会更慢的唯一原因是:

代码将循环优化为 2 . CMP eax 当减法发生时,你实际上是在减法 A. 00000000 - 00000010 CMP eax, 2 而不是 B. 00000000 - 00000001 CMP eax, 1

CMP只设置标志,不设置结果。所以对于最低有效位,我们知道b是否需要借位,而对于a,你必须做两次减法才能得到借位。

这取决于编译器。

如果它优化了代码,或者对于一个特定的指令集,如果它用相同数量的指令计算1和2为真,那么执行速度将是相同的。

在实际情况下,它总是同样快,但可以想象在特定的编译器和特定的系统中,这将得到不同的计算。

我的意思是:这不是一个与语言(C)相关的问题。

对这个问题最有可能的解释是,面试官认为处理器会逐一检查数字的每一位,直到它达到一个非零值:

1 = 00000001
2 = 00000010

如果“is 0 ?”算法从数字的右侧开始,并且必须检查每一位直到它达到非零位,那么while(1){}循环每次迭代必须检查的比特数是while(2){}循环的两倍。

这需要一个关于计算机如何工作的非常错误的思维模型,但它确实有自己的内部逻辑。一种检查方法是询问while(-1){}或while(3){}是否同样快,或者while(32){}是否更慢。

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

使用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标签,甚至不将值与true进行比较,当然,它会立即再次这样做,直到程序以某种方式结束。这直接对应于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()) {}

At -O0, these two examples actually call x and perform a comparison for each iteration.

First example (returning 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"

Second example (returning 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"

However, at -O1 and above, they both produce the same assembly as the previous examples (an unconditional jmp back to the preceding label).

TL;DR

Under GCC, the different loops are compiled to identical assembly. The compiler evaluates the constant values and doesn't bother performing any actual comparison.

The moral of the story is:

  • There exists a layer of translation between C source code and CPU instructions, and this layer has important implications for performance.
  • Therefore, performance cannot be evaluated by only looking at source code.
  • The compiler should be smart enough to optimize such trivial cases. Programmers should not waste their time thinking about them in the vast majority of cases.

等一下。面试官长得像这个人吗?

面试官自己没能通过面试已经够糟糕的了, 如果这家公司的其他程序员已经“通过”了这个测试呢?

不。求值语句1 == 0和2 == 0应该同样快。我们可以想象,在糟糕的编译器实现中,一个可能比另一个快。但是,没有充分的理由说明其中一个应该比另一个快。

即使在某些模糊的情况下,这种说法是正确的,也不应该基于模糊(在这种情况下,令人毛骨悚然)琐事的知识来评估程序员。不要担心这次面试,最好的办法就是走开。

免责声明:这不是原创呆伯特漫画。这只是一个混搭。