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

616

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

哪一个更快?

while(1) {
    // Some code
}
while(2) {
    //Some code
}

我说两者的执行速度相同,因为while语句内的表达式最终应该会评估为truefalse。在这种情况下,两者都评估为true,并且while循环条件内没有额外的条件指令。因此,两者的执行速度相同,我更喜欢while(1)

但面试官自信地说: “检查一下你的基础知识。while(1)while(2)更快。” (他并不是在测试我的自信心)

这是真的吗?

另请参阅:如果不是这样,为什么人们使用“for(;;)”?


216
一款还算不错的编译器会将这两种形式都优化成无操作。 - user1864610
72
在优化编译中,每当出现“while(n), n != 0”或“for(;;)”时,都会被翻译成带有标签的汇编无限循环,并在结尾处使用“goto”。这段代码与原始代码完全相同,性能也相同。 - Alex F
66
毫不奇怪,对股票进行优化后,两个片段都会出现 0x100000f90: jmp 0x100000f90(地址显然会有所变化)。面试官可能在寻找一个寄存器测试或简单标记跳转之间权衡。这个问题和他们的推测都很无聊。 - WhozCraig
54
面试官的问题与 http://dilbert.com/strips/comic/1995-11-17/ 相似 - 你会遇到某个人,无论他们的言论有多么愚蠢,他们都真心相信自己说的话。只需从以下选项中选择一个:深呼吸,发誓,笑,哭或以上几种的组合 :) - GMasucci
3
@Mike W: 一个人可能会想,编译器应该做什么:将代码翻译为停机语句,还是考虑循环在无限时间后退出并优化掉无限延迟? - user1196549
显示剩余15条评论
23个回答

718

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

使用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及以上级别,它们都会产生与之前示例相同的汇编代码(无条件跳转到前一个标签)。

TL;DR

在GCC下,不同的循环被编译为相同的汇编代码。编译器对常量值进行评估,并不会执行任何实际比较操作。

故事的寓意是:

  • 存在着将C语言源代码和CPU指令之间的转换层次,这个层次对性能有重要影响。
  • 因此,仅通过查看源代码不能评估性能。
  • 编译器应该足够聪明以优化这种琐碎的情况。在绝大多数情况下,程序员 不应该浪费时间考虑这些问题。

211
可能面试官没有使用gcc编译器。 - M.M
126
这是一个好观点,但如果面试官依赖于特定于编译器的优化,那么他们需要在问题中非常明确地表达出来,并且他们需要接受"没有区别"的答案作为一些(大多数?)编译器的正确答案。 - Jonathan Hartley
119
为了消除任何疑虑,我在clang 3.4.2中进行了测试,两个循环在每个“-O”级别下都会产生相同的汇编代码。 - Martin Tournoij
19
我并不感到惊讶,因为你在循环的条件部分放置的所有内容都是编译时常量。因此,我会怀疑编译器会看到循环将始终为真或假,并分别简单地跳转回开头或完全删除循环。 - sherrellbc
12
@hippietrail,我不认为循环的内容(或缺乏内容)可能会影响这些优化(除了任何break语句的可能性),但无论如何我已经进行了测试,结果是:即使在循环内部有代码,在while(1)while(2)中跳跃都是绝对且无条件的。如果你真的担心,可以自己测试其他情况。 - ApproachingDarknessFish
显示剩余13条评论

315

是的,while(1)while(2)快得多,对于人来说至少如此!如果我在一个陌生的代码库中看到while(1),我立即知道作者的意图,我的眼球可以继续看下一行。

如果我看到while(2),我可能会停下来试图弄清楚为什么作者没有写while(1)。作者的手指是否误触了键盘?这个代码库的维护者是否使用while(n)作为一种深奥的注释机制来使循环看起来不同?它是某个损坏的静态分析工具中哗众取宠的警告的粗略解决方法吗?还是这是一个线索,表明我正在阅读生成的代码?这是由于不明智的查找和替换全部、错误合并或宇宙射线导致的错误吗?也许这行代码应该做一些截然不同的事情。也许它原本应该读while(w)while(x2)。我最好在文件的历史记录中找到作者,并发送一封“WTF”电子邮件……现在我打破了我的思维上下文。当while(1)只需要一小部分时间时,while(2)可能会消耗几分钟的时间!

虽然有点夸张,但代码的可读性确实非常重要。这值得在面试中提及!


8
毫不夸张地说,这里绝对不会夸大其词。肯定会使用 svn-annotategit blame(或其他类似的工具),通常只需要几分钟即可加载文件的历史记录。然后最后只需决定“啊,我明白了,作者在高中毕业时写下了这一行”,只用花费十分钟左右。 - v.oddou
16
因为我厌倦了常规,所以投票反对。开发者有机会写他喜欢的数字,但总有一些无聊的人会唠叨说它为什么不是“1”。 - Utkan Gezer
5
因为修辞手法被踩了。无限循环中的 while(1) 和 while(2) 速度完全相同。 - hdante
6
当我看到代码中的 while(2) 时,我经历了和这个答案描述的完全相同的心理过程(包括考虑“这个代码库的维护者是否使用 while(n) 作为一种模糊的注释机制来使循环看起来不同?”)。所以,不,你并没有夸张! - Hisham H M
1
@CacahueteFrito:也许有些编辑器会这样做,但并不是所有的编辑器都会。例如,emacs默认情况下不会为数字和变量使用不同的颜色。qemacs也不会,而我的个人着色选择为变量和数字使用略微不同的绿色阴影,这并不能解决歧义问题。错误消息也不总是着色的。在所有情况下使用明确的名称可以解决问题。 - chqrlie
显示剩余7条评论

156
现有的答案展示了针对特定目标、一组特定选项下特定编译器所生成的代码,但这并不能完全回答问题——除非问题是在特定背景下提出的(例如,“使用默认选项的gcc 4.7.2编译x86_64哪个更快?”)。
就语言定义而言,在“抽象机器”中,while (1)计算整数常量1while (2)计算整数常量2,在两种情况下,结果都与零进行比较。语言标准对于这两种构造相对性能绝对没有任何说明。
我可以想象,一个极其天真的编译器可能会在两种形式中为它们生成不同的机器码,至少在没有请求优化时编译的情况下如此。
另一方面,C编译器必须绝对在编译时计算某些常量表达式,当它们出现在需要常量表达式的上下文中时。例如:
int n = 4;
switch (n) {
    case 2+2: break;
    case 4:   break;
}

需要进行诊断;懒惰的编译器不能推迟对2+2表达式的评估,直到执行时刻。由于编译器必须具备在编译时评估常量表达式的能力,因此即使没有此要求,在不需要时也应该利用这种能力。

C标准(N1570 6.8.5p4)规定:

循环语句会导致名为loop body的语句被重复执行,直到控制表达式比较等于0。

因此相关的常量表达式是1 == 02 == 0,两者都将评估为int0。(这些比较隐含在while循环的语义中;它们并不存在于实际的C表达式中.)

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

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

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

最终结论:while (1)while (2) 几乎具有相同的性能。它们具有完全相同的语义,没有任何理由让任何编译器不生成相同的代码。

虽然编译器为while(1)生成更快的代码是完全合法的,但编译器也可以为同一程序中的另一个while(1)生成更快的代码。

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


8
在这种情况下,相关的(隐式)常量表达式是1 != 0和2 != 0,它们都评估为int值1。这段话过于复杂,且不准确。标准只是说while的控制表达式必须是标量类型,并且在表达式比较为0之前重复循环体。它并没有说有一个隐式的'expr != 0'会被评估...那样需要将其结果-0或1-与0无限比较。不,表达式被与0比较,但是该比较不产生值。附言:我已投赞成票。 - Jim Balter
3
@JimBalter:我理解你的观点,我会更新我的答案来回应它。但是,我的意思是标准的措辞“...直到控制表达式与0相等”意味着要评估<expr> == 0;这就是C语言中“与0相等”*的含义。 这种比较是while循环的语义的一部分。 标准和我的答案都没有暗示需要再次将结果与0进行比较。(我应该写==而不是!=。)但是,我的答案在这方面并不清楚,我会更新它。 - Keith Thompson
2
@JimBalter:嗯,我并不是要暗示“等于0”意味着存在一个... == 0的C表达式。我的观点是,标准对while循环的描述中所需的“等于0”和显式的x == 0表达式在逻辑上暗示了相同的操作。而且我认为,一个极其天真的C编译器可能会为任何while循环生成一个生成int值为01的代码,尽管我不认为有任何实际的编译器会如此天真。 - Keith Thompson
1
我并不是要表达...但你确实这样做了,而且当你写下“我认为一个非常幼稚的C编译器可能会为任何while循环生成一个int值为0或1的代码”时,你又再次这样做了。请看看实际上一个非常幼稚的编译器在anatolyg的回答中生成的代码:在目标机器上,而不是在C中,对控制表达式和零进行显式比较。这就是标准所要求的,不是评估产生0或1的某个C表达式。如果你还是不明白,进一步的讨论也无济于事,所以我在这里结束了。 - Jim Balter
12
好的,我会尽力进行翻译。以下是需要翻译的内容:注意:这已经是一个[工作场所.交流]的问题:http://workplace.stackexchange.com/questions/4314/how-to-tell-a-interviewer-that-he-is-wrong-on-a-technical-question如何告诉面试官他在技术问题上的错误? - Joe
显示剩余8条评论

150

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

enter image description here

糟糕的是,面试官本身也没能通过这次面试,如果这家公司的其他程序员“通过”了这个测试呢?

不。评估语句 1 == 02 == 0 应该是同样快的。我们可以想象存在某些糟糕的编译器实现,其中一个比另一个要快。但没有充分的理由之一应该比另一个更快。

即使在一些模糊情况下,该主张是正确的,程序员也不应该根据对模糊(而且在这种情况下是令人毛骨悚然的)琐事的知识来进行评估。不必担心这次面试,最好的做法是离开。

免责声明: 这不是原始的 Dilbert 漫画。这只是一个混搭作品


不过,我们可以很容易地想象,所有由严肃公司编写的编译器都会生成合理的代码。让我们来看看“非优化情况”/O0,也许它最终会像anatolyg发布的那样。然后就是CPU的问题了,将cmp操作数与0比较时,1和0相比较是否需要更少的周期?执行cmp需要多少个周期?它是否根据位模式变量?是否有更“复杂”的位模式会减慢cmp的速度?我个人不知道。你可以想象一个超级白痴的实现方式,从等级0到n(例如n=31)逐位检查。 - v.oddou
5
我的观点也是:cmp操作数对于1和200应该是同样快的。可能我们可以想象一些愚蠢的实现,使得这种情况不成立。但是我们能想象出一个非愚蠢的实现,在这个实现中while(1)while(200)更快吗?同样地,如果在某个史前时代,唯一可用的实现是像那样愚蠢的,那么今天我们还需要对此大惊小怪吗?我认为不需要,这就是“尖头老板”的话,而且真正闪耀! - janos
@v.ouddou “比较操作数1和0与2和0的周期是否会更少” - 不会。您应该了解什么是一个周期。“我个人不知道。你可以想象一种超级愚蠢的实现方式,从第0到n位逐位检查” - 或者反过来,仍然让面试官感到无知。为什么要担心逐位检查?实现方式可能是一个盒子里的人,在评估您的程序时决定中午休息一下。 - Jim Balter
将0或1加载到寄存器中,根据CPU架构可能比加载2更快。但是提出这个问题的人显然没有意识到循环中的测试不会编译成任何东西。 - Joshua

83

你的解释是正确的。除了技术知识外,这似乎是一道测试你自信心的问题。

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

两个代码片段的速度相同,因为两者都需要无限的时间才能完成

面试官会说:

但是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

所以生成的代码是一模一样的,至少对于一个优化编译器而言。


28
这段代码确实是在我的系统上编译器输出的,我没有胡乱捏造。 - anatolyg
11
"while的操作数是布尔类型"这种说法完全无稽之谈。你是面试官吗?我建议你在发表此类言论之前先熟悉C语言及其标准。Icepack" - Jim Balter
30
“我没有编造。”-- 请不要理会胡说八道的icepack。C语言没有布尔类型(虽然stdbool.h中有_Bool,但它不同,并且while的语义早在其之前就产生了),而while的操作数不是布尔、_Bool或任何其他特定类型。while的操作数可以是任何表达式...while在0上中断,在非0上继续执行。 - Jim Balter
38
“两段代码的速度是一样的,因为它们都需要无限的时间才能完成”的说法让我想到了一些有趣的事情。唯一终止无限循环的方式是由带电粒子翻转一个比特或是硬件出现故障:将语句从while (00000001) {}更改为while (00000000) {}。你拥有的真值位数越多,值翻转为假的概率就越小。遗憾的是,2只有一个真值位。然而,3会运行更长的时间。这仅适用于不总是优化此类代码(VC++)的编译器。 - Jonathan Dickinson
8
@mr5 不对。要让比特位翻转实际上导致像你所说的这样的结果,你需要考虑数万年的执行时间。这只是一个"思想实验"。如果你来自一种不朽的种族,你可能想使用“-1”来防止比特翻转影响你的程序。 - Jonathan Dickinson
显示剩余12条评论

64

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

1 = 00000001
2 = 00000010

如果“是否为零?”算法从数字的右侧开始,并且必须检查每个位,直到达到非零位,则 while(1) { } 循环每次迭代需要检查的位数是 while(2) { } 循环的两倍。

这需要一种非常错误的计算机工作模型,但它确实有其内在的逻辑。一种检查的方法是问 while(-1) { }while(3) { } 是否同样快,或者 while(32) { }更慢


41
我假设面试官误解的更多是“2 是一个整数,需要转换为布尔值才能在条件表达式中使用,而 1 已经是布尔值。”请让我知道是否需要进一步翻译。 - Russell Borogove
7
如果比较算法从左侧开始,情况就相反了。 - Paŭlo Ebermann
1
没错,这正是我所想的。你可以很好地想象有人相信CPU的cmp算法本质上是一种线性位检查,并在第一个差异处提前退出循环。 - v.oddou
1
@PeterCordes:“我从未听说过有人(除了你)认为gcc或clang的-O0输出不够准确。” 我从未说过这样的话,而且我根本没有考虑过gcc或clang。 你一定误解了我的意思。 我只是不同意你认为MSVC生成的东西很好笑。 - blubberdiblub
1
@PeterCordes 我错了,在MSVC中,对while(1)的断点不会在循环之前中断,而是在表达式上中断。但是当优化表达式时,它仍然会在循环内部崩溃。看看这个代码,里面有很多断点。在未经优化的调试模式下,你会得到这个结果。当进行优化时,许多断点会合并或甚至溢出到下一个函数中(IDE在调试时显示结果断点)- 相应的反汇编 - blubberdiblub
显示剩余14条评论

36

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

他们将你推入冲突中。如果这是真的,他们很聪明,而这个问题也很好。对于某些行业,如银行业,将你的问题发布到Stack Overflow上可能成为被拒绝的原因。

但当然我不知道,我只提出了一种选择。


2
这确实很棒,但while(2) vs while(1)显然是从迪尔伯特漫画中取得的。这不可能是正常人发明的(无论如何,任何人怎么可能想到写while(2)呢?)。如果你的假设是正确的,那么你肯定会提出一个如此独特的问题,以至于你可以在谷歌上搜索到它。比如“while(0xf00b442)是否比while(1)慢”,银行怎么能找到面试者的问题呢?你觉得他们是NSA,并拥有Keyscore的访问权限吗? - v.oddou

29

我认为关键在于“由一位高级经理提出的问题”。这个人显然在成为经理后停止了编程,然后花费了几年时间成为高级经理。他从未失去对编程的兴趣,但自那时起就再也没有写过一行代码。因此,他的参考对象不是像某些答案所提到的“任何体面的编译器”,而是“这个人20-30年前使用的编译器”。

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

最好的求职方法可能是让这位高级经理兴致勃勃地讲述“编程的好旧时光”,然后你巧妙地引导他转向下一个面试主题。在这里,好的时间掌握非常重要--太快了就会打断故事,太慢了就会被认为是缺乏专注力的人。在面试结束时,确实告诉他你对这个主题非常感兴趣。


22

你应该问他是如何得出结论的。在任何一个体面的编译器中,这两种写法都会编译成相同的汇编指令。所以,他还应该告诉你使用的编译器。即使如此,你也需要非常了解编译器和平台才能做出理论上的有根据的猜测。而从实际角度来看,这个细节并不重要,因为其他外部因素,例如内存碎片化或系统负载,会对循环产生更大的影响。


15
@GKFX 如果你已经回答了问题,但他们告诉你答案错误,那么你完全可以要求他们解释为什么。如果Anatolyg是正确的,这是对你自信心的考验,那么你应该解释为什么你回答了这个问题,并问他们同样的问题。 - P.Turpie
我的意思是你对他们说的第一件事。它不能是“为什么x更快?”“我不知道;为什么x更快?”显然,在正确回答后,你可以再问。 - GKFX

20

就这个问题而言,我应该补充一下,我记得 C++ 委员会的 Doug Gwyn 曾经写过,一些早期的没有优化器功能的 C 编译器在汇编中为 while(1) 生成了一个测试(相比于没有这个测试的 for(;;))。

我会告诉面试官这段历史背景,并且说即使我很惊讶会有编译器这样做, 理论上编译器是可以这样做的:

  • 如果没有优化器,编译器将为 while(1)while(2) 都生成一个测试
  • 如果使用了优化器,编译器会根据惯用语法将所有的 while(1) 进行优化(使用无条件跳转),这会导致 while(2) 包含一个测试,从而在两者之间产生性能差异。

当然,我还会告诉面试官,不将 while(1)while(2) 视为相同的结构是低质量优化的表现,因为它们是等价的结构。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接