哪一个更快:while(1)还是while(2)?

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

哪个更快?

while(1) {
// Some code
}

while(2) {
//Some code
}

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

但是面试官自信地说: 检查你的基本知识。while(1)while(2)快。 (他不是在考验我的信心)

这是真的吗?

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

95980 次浏览

你的解释很正确。这似乎是一个在技术知识之外考验你自信的问题。

顺便说一下,如果你回答了

这两段代码同样快,因为它们都需要无限长的时间来完成

面试官会说

但是while (1)每秒可以做更多的迭代;你能解释一下原因吗?(这是无稽之谈;再次考验你的信心)

所以,通过像你那样回答,你节省了一些时间,否则你会浪费在讨论这个糟糕的问题上。


下面是编译器在我的系统(MS Visual Studio 2012)上生成的示例代码,优化关闭:

yyy:
xor eax, eax
cmp eax, 1     (or 2, depending on your code)
je xxx
jmp yyy
xxx:
...

打开优化后:

xxx:
jmp xxx

因此生成的代码是完全相同的,至少在优化编译器中是如此。

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

使用gcc,我编译了以下两个程序,以不同的优化级别进行汇编:

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

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

因此,两个循环之间没有速度差异。

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

与# EYZ0:

    .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"

与# EYZ0:

    .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()) {}

-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;

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

这个故事的寓意是:

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

你应该问问他是怎么得出那个结论的。在任何像样的编译器中,两者都编译为相同的asm指令。他应该在一开始就告诉你编译器。即便如此,您也必须非常了解编译器和平台,才能做出理论上的有根据的猜测。最后,在实践中这并不重要,因为还有其他外部因素,如内存碎片或系统负载,会比这个细节更大地影响循环。

这里有一个问题:如果您实际编写一个程序并测量它的速度,两个循环的速度可能是不同的!以下是一些合理的比较:

unsigned long i = 0;
while (1) { if (++i == 1000000000) break; }


unsigned long i = 0;
while (2) { if (++i == 1000000000) break; }

添加了一些打印时间的代码后,一些随机的效果(比如循环在一条或两条缓存线中的位置)可能会产生影响。一个循环可能完全位于一条缓存线内,或者位于一条缓存线的开头,或者它可能横跨两条缓存线。因此,面试官说最快的可能真的是最快的——纯属巧合。

最坏的情况:优化编译器不知道循环做了什么,但知道执行第二个循环时产生的值与第一个循环产生的值相同。并为第一个循环生成完整代码,但不为第二个循环生成完整代码。

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

就语言定义而言,在抽象机器中,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)就是这么说的

一个迭代语句导致一个名为循环体的语句 重复执行,直到控制表达式比较为 < / p > 0。

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

一个非常简单的编译器可以为这两种构造生成不同的代码。例如,对于第一种情况,它可以生成一个无条件的无限循环(将1视为特殊情况),对于第二种情况,它可以生成与2 != 0等效的显式运行时比较。但我从来没有遇到过这样的C编译器,我严重怀疑这样的编译器是否存在。

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

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

底线:while (1)while(2) 几乎当然具有相同的性能。它们具有完全相同的语义,任何编译器都没有理由不生成相同的代码。

尽管编译器为while(1)生成比while(2)更快的代码是完全合法的,但编译器为while(1)生成比在同一个程序中出现while(1)更快的代码也是同样合法的。

(你问的问题中还暗含着另一个问题:你如何应对坚持提出一个错误的技术观点的面试官?这可能是职场网站的一个好问题)。

它们是相等的。

根据规范,任何不为0的都被认为是真的,所以即使没有任何优化,一个好的编译器也不会生成任何代码 For while(1) or while(2)。编译器将生成一个简单的检查!= 0.

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

1 = 00000001
2 = 00000010

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

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

当然,我不知道这位经理的真实意图,但我提出了一个完全不同的观点:当一个团队雇佣一个新成员时,了解他如何应对冲突情况是很有用的。

他们让你陷入冲突。如果这是真的,他们很聪明,这个问题也很好。对于一些行业,比如银行业,把你的问题发布到stack Overflow上可能会成为被拒绝的一个原因。

但我当然不知道,我只是提出一个选择。

是的,while(1)while(2)为了人类的阅读!快得多,如果我在一个不熟悉的代码库中看到while(1),我马上就知道作者想要什么,我的眼球可以继续看下一行。

如果我看到while(2),我可能会停下来,试着弄清楚为什么作者没有写while(1)。作者的手指在键盘上滑倒了吗?这个代码库的维护者是否使用while(n)作为一种模糊的注释机制来使循环看起来不同?它是某些坏掉的静态分析工具中虚假警告的粗糙变通方法吗?或者这是我正在阅读生成代码的线索?这是一个错误的“查找并替换所有”,还是一个错误的合并,或者是宇宙射线?也许这行代码应该做一些完全不同的事情。也许它应该读while(w)while(x2)。我最好在文件的历史记录中找到作者,然后给他们发一封“WTF”电子邮件……现在我的思维环境被打破了while(2)可能会占用我几分钟的时间,而while(1)可能只花了几分之一秒!

我有点夸张,但也只是一点点。代码的可读性非常重要。这一点在面试中很值得一提!

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

  1. 代码优化循环到

    # EYZ0 < / p >

  2. 当减法发生时,你实际上是在做减法

    一。# EYZ0

    而不是

    b。# EYZ0 < / p >

cmp只设置标志,不设置结果。因此,在最不重要的位上,我们知道是否需要借用b。而对于一个,你必须执行两次减法才能获得借位。

我认为线索可以从“高级经理的询问”中找到。这个人显然在成为经理后就停止了编程,然后他/她花了几年时间才成为高级经理。从未失去对编程的兴趣,但从那以后再也没有写过一行字。所以他提到的并不是“任何一个像样的编译器”,而是“这个人在二三十年前合作过的编译器”。

在那个时候,程序员花了相当大的比例的时间尝试各种方法,以使他们的代码更快、更有效,因为“中央小型计算机”的CPU时间是如此宝贵。编写编译器的人也是如此。我猜他的公司当时提供的唯一的编译器是基于“经常遇到的可以优化的语句”进行优化的,并且在遇到while(1)时走了一点捷径,并评估了其他所有内容,包括while(2)。有了这样的经历,就可以解释他的立场和信心了。

让你被录用的最好方法可能是让高级经理得意忘形,在你引导他进入下一个面试主题之前,给你讲2-3分钟“编程的美好时光”。(好的时机在这里很重要——太快你会打断故事——太慢你会被贴上焦点不集中的标签)。一定要在面试结束时告诉他,你对这个话题非常感兴趣。

关于这个问题的另一个答案是,看看你是否有勇气告诉你的经理他/她错了!以及你能多温柔地交流。

我的第一直觉是生成汇编输出,向管理器显示任何像样的编译器都应该照顾它,如果它不这样做,你将提交它的下一个补丁:)

这取决于编译器。

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

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

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

看到这么多人钻研这个问题,正好说明了为什么这可以很好地作为一个测试,看看您想要多快地完成micro-optimize事情。

我的答案是;这并不重要,我更关注我们正在解决的业务问题。毕竟,这就是我的工作。

此外,我会选择while(1) {},因为它更常见,其他队友不需要花时间去弄清楚为什么有人会选择比1更高的数字。

现在去写一些代码。: -)

为了这个问题,我应该补充一点,我记得C委员会的Doug Gwyn写过,一些早期的C编译器如果没有优化器通过,会为while(1)生成一个汇编测试(相比之下,for(;;)没有)。

我会给出这个历史笔记来回答面试官,然后说,即使我对任何编译器这样做感到非常惊讶,编译器也可以这样做:

  • 在没有优化器的情况下,编译器为while(1)while(2)生成一个测试
  • 通过优化器,编译器被指示优化(无条件跳转)所有while(1),因为它们被认为是惯用的。这将给while(2)留下一个测试,从而使两者之间的性能有所不同。

我当然要向面试官补充一点,不将while(1)while(2)视为相同的结构是低质量优化的标志,因为它们是等效的结构。

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

enter image description here

面试官自己失败了这次采访已经够糟糕的了, 如果这家公司的其他程序员已经“通过”了这个测试呢?< / p >

不。同样快速地计算语句1 == 02 == 0 应该是。我们可以想象糟糕的编译器实现,其中一个可能比另一个更快。但是没有任何理由可以解释为什么一个会比另一个快。

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

这不是原版呆伯特漫画。这只是一个混搭.

根据人们花费在测试、证明和回答这个非常直接的问题上的时间和精力来判断,我认为这两者都因为提出这个问题而变得非常缓慢。

所以花更多的时间在这上面…

while (2)是荒谬的,因为,

while (1)while (true)在历史上被用于创建一个无限循环,根据一定会发生的条件,期望在循环中的某个阶段调用break

1只是为了总是求值为真,因此,说while (2)和说while (1 + 1 == 2)一样愚蠢,它也会求值为真。

如果你想完全傻一点,就用:-

while (1 + 5 - 2 - (1 * 3) == 0.5 - 4 + ((9 * 2) / 4.0)) {
if (succeed())
break;
}

我认为面试官犯了一个拼写错误,这并不影响代码的运行,但如果他故意使用2只是为了表现得奇怪,那么在他把奇怪的语句贯穿你的代码,使其难以阅读和使用之前解雇他。

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

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

在我看来,这是一个被伪装成技术问题的行为面试问题。有些公司会这样做——他们会问一个任何有能力的程序员都应该很容易回答的技术问题,但是当面试者给出正确答案时,面试官会告诉他们他们错了。

公司想看看你在这种情况下会作何反应。你是否会因为自我怀疑或害怕让面试官不高兴而安静地坐在那里,不强调自己的答案是正确的?或者你愿意挑战一个你知道是错误的权威人士吗?他们想看看你是否愿意坚持自己的信念,以及你是否能以一种机智和尊重的方式做到这一点。

也许面试官是故意问这个愚蠢的问题,想让你回答3点:

  1. 这两个循环都是无限的,很难谈论性能。
  2. 关于优化级别的知识。他想听听你的意见,如果你让编译器为你做任何优化,它会优化条件,特别是如果块不是空的。
  3. 大多数架构都有一个特殊的CPU指令用于与0进行比较(但不一定更快)。

如果你担心优化,你应该使用

for (;;)

因为它没有检验。(愤世嫉俗者模式)

因为回答这个问题的人希望得到最快的循环,所以我的回答是,两者都是同样地编译成相同的汇编代码,正如在其他答案中所述。不过,你可以建议面试官使用循环展开的;do {} while循环而不是while循环。

谨慎:# EYZ0。

循环内部应该有一个中断条件。

同样,对于这种循环,我个人更喜欢使用do {} while(42),因为除了0之外,任何整数都可以完成这项工作。

显而易见的答案是:正如所发布的,两个片段将运行一个同样繁忙的无限循环,这使得程序无限地

虽然从技术上讲,将C关键字重新定义为宏将具有未定义的行为,但这是我能想到的使任何一个代码片段快速的唯一方法:你可以在两个片段之上添加这一行:

#define while(x) sleep(x);

它确实会使while(1)while(2)快两倍(或慢一半)。