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

Nik*_*le 582 c performance while-loop

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

哪个更快?

while(1) {
    // Some code
}
Run Code Online (Sandbox Code Playgroud)

要么

while(2) {
    //Some code
}
Run Code Online (Sandbox Code Playgroud)

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

但采访者自信地说:"检查你的基础知识.while(1)比快while(2)." (他没有测试我的信心)

这是真的?

另请参阅:"for(;;)"是否比"while(TRUE)"快?如果没有,为什么人们会使用它?

App*_*ish 676

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

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

int main(void) {
    while(1) {}
    return 0;
}
Run Code Online (Sandbox Code Playgroud)


int main(void) {
    while(2) {}
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

即使没有优化(-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"
Run Code Online (Sandbox Code Playgroud)

-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"
Run Code Online (Sandbox Code Playgroud)

-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"
Run Code Online (Sandbox Code Playgroud)

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

 .L2:
    jmp .L2
    .seh_endproc
    .ident  "GCC: (tdm64-2) 4.8.1"
Run Code Online (Sandbox Code Playgroud)

重要的是:

.L2:
    jmp .L2
Run Code Online (Sandbox Code Playgroud)

我不能很好地阅读汇编,但这显然是一个无条件的循环.该jmp指令无条件地将程序重置回.L2标签,甚至没有将值与true进行比较,当然也会立即再次执行,直到程序以某种方式结束.这直接对应于C/C++代码:

L2:
    goto L2;
Run Code Online (Sandbox Code Playgroud)

编辑:

有趣的是,即使没有优化,以下循环都会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) {}
Run Code Online (Sandbox Code Playgroud)

令我惊讶的是:

#include<math.h>

while(sqrt(7)) {}

while(hypot(3,4)) {}
Run Code Online (Sandbox Code Playgroud)

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

int x(void) {
    return 1;
}

while(x()) {}
Run Code Online (Sandbox Code Playgroud)


#include<math.h>

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

while(x()) {}
Run Code Online (Sandbox Code Playgroud)

-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"
Run Code Online (Sandbox Code Playgroud)

第二个例子(返回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"
Run Code Online (Sandbox Code Playgroud)

但是,-O1除此之外,它们都生成与前面示例相同的程序集(无条件地jmp返回到前面的标签).

TL; DR

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

这个故事的寓意是:

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

  • 也许面试官没有使用gcc (195认同)
  • 为了消除任何疑问,我在clang 3.4.2中对此进行了测试,并且两个循环在每个`-O`级别生成相同的程序集. (107认同)
  • @Matt McNabb这是一个很好的观点,但如果面试官依赖于编译器特定的优化,那么他们需要在他们的问题中非常明确地说明这一点,并且他们需要接受"没有差异"的答案是正确的一些(大多数?)编译器. (104认同)
  • 我没有发现这一点令人惊讶,因为你在循环的条件部分放置的所有内容都是编译时常量.因此,我怀疑编译器会看到循环将始终为true或false,并且分别只是简单地将`jmp`返回到开头或完全删除循环. (16认同)
  • @hippietrail我没有看到循环的内容(或缺少)可能会影响这些优化(除了任何`break`语句的可能性),但我只是测试它而不是,即使代码在循环内部对于`while(1)`和`while(2)`,跳转是绝对的和无条件的.如果您真的担心的话,请随意亲自测试其他人. (10认同)
  • @Carpetsmoker:这消除了对clang 3.4.2在您使用它的特定配置中的行为的任何疑问. (3认同)
  • 仅供参考,`.seh_endproc`和`.ident"GCC:(tdm64-2)4.8.1"`不是循环汇编语言的一部分.第一个标记当前函数的结束,第二个标记使用创建它的编译器的标识来标记目标文件.(通常会有一个"函数结尾",清除堆栈帧并在循环和`.seh_endproc`之间发出`ret`指令,但编译器已经意识到控件永远不会超过无限循环,并且因此将其省略为不必要.) (3认同)
  • @MattMcNabb这是一个好点; 不幸的是,我的系统上没有安装任何其他编译器.如果有人可以测试clang,msvs或其他任何东西,我会很想看到结果. (2认同)
  • `所以测量他们的"速度"有点荒谬`,你叫? (2认同)
  • 这里的基本前提似乎是:所有编译器,至少,将选择`cmp eax,0`(或本地机器相当于"结果等于零")作为首选算法,并且高级分析(优化)几乎肯定会产生无条件的跳跃.编译器执行任何其他操作的几率似乎实际上是荒谬的,因为这是实现测试的最简单方法. (2认同)
  • 如果面试官没有使用`gcc`,他们应该问**哪个版本在*我的*编译器上更快**。 (2认同)

Chr*_*ter 277

是的,while(1)比快得多while(2),对于人类阅读!如果我while(1)在一个不熟悉的代码库中看到,我立即知道作者的意图,我的眼球可以继续下一行.

如果我看到while(2),我可能会停下来试图找出作者没写的原因while(1).作者的手指在键盘上滑动了吗?这个代码库的维护者是否使用while(n)了一个模糊的注释机制来使循环看起来不同?对于某些破碎的静态分析工具中的虚假警告,这是一个粗略的解决方法吗?或者这是我正在阅读生成代码的线索吗?这是一个由于不明智的查找和替换全部,或者是一个糟糕的合并或一个宇宙射线导致的错误?也许这行代码应该做一些截然不同的事情.也许应该阅读while(w)while(x2).我最好在文件的历史中找到作者并向他们发送"WTF"电子邮件......现在我已经打破了我的心理背景.将while(2)可能会消耗我的时间几分钟,当while(1)将采取第二的分数!

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

  • 投票,因为我厌倦了惯例.开发人员有这个微不足道的机会写一个他喜欢的数字,然后有人无聊而且唠叨为什么它不是"1". (11认同)
  • 当然,这一点都不夸张.绝对会在这里使用`svn-annotate`或`git blame`(或其他),一般来说加载文件责备历史需要几分钟.然后才最后决定"啊我理解,作者在高中毕业后写了这条线",刚丢了10分钟...... (8认同)
  • 一分钟之前,当我在代码中看到`while(2)`时(直到考虑"这个代码库的维护者使用while(n)作为一个模糊的评论机制),我经历了与此答案中描述的完全相同的心理过程make循环看起来不一样?").所以不,你不夸张! (4认同)
  • 由于言辞而被否决。读取 while(1) 的速度与 while(2) 完全相同。 (3认同)

Kei*_*son 151

显示特定编译器为具有特定选项集的特定目标生成的代码的现有答案不能完全回答问题 - 除非在该特定上下文中询问了问题("使用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;
}
Run Code Online (Sandbox Code Playgroud)

需要诊断; 懒惰的编译器没有选择将评估推迟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)在相同的程序.

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

  • 注意:这已经是[workplace.se]问题:http://workplace.stackexchange.com/questions/4314/how-to-tell-a-interviewer-that-he-is-wrong-on-a-technical -题 (11认同)
  • "在这种情况下,相关的(隐式)常量表达式是1!= 0和2!= 0,两者都计算为int值1"......这过于复杂,而且不准确.标准只是说`while`的控制表达式必须是标量类型,循环体重复,直到表达式比较等于0.它没有说有一个隐含的`expr!= 0`被评估...这将需要将结果 - 0或1 - 反过来与无穷无尽的0进行比较.不,表达式与0进行比较,但该比较不会产生值.PS我投了赞成票. (8认同)
  • @JimBalter:我明白你的意思了,我会更新我的答案来解决它.但我的意思是标准的措辞"......直到控制表达式比较等于0"意味着评估`<expr> == 0`; 这就是"比较等于0"*意味着*在C中.这种比较是`while`循环的语义的一部分.无论是在标准中还是在我的答案中,都没有暗示结果需要再次与"0"进行比较.(我应该写'==`而不是`!=`.)但我的答案部分还不清楚,我会更新它. (3认同)
  • @JimBalter:嗯.我并不是说"比较等于0"意味着存在一个`... == 0` C表达式.我的观点是标准对`while`循环的描述所要求的"比较等于0"和明确的`x == 0`表达式在逻辑上意味着相同的操作.而且我认为一个*痛苦的天真的C编译器可能会生成为任何`while`循环生成`int`值为'0`或`1`的代码 - 尽管我不相信任何实际的编译器都是那么天真. (2认同)

Sto*_*ica 137

等一下.面试官,他看起来像这个家伙?

在此输入图像描述

面试官本人未能通过这次面试,这很糟糕,如果这家公司的其他程序员"通过"了这个测试呢?

不.评估陈述1 == 0并且2 == 0 应该同样快.我们可以想象糟糕的编译器实现,其中一个可能比另一个更快.但是没有充分的理由说为什么一个人应该比另一个人更快.

即使有一些模糊的情况,当声称是真的,程序员也不应该根据模糊(在这种情况下,令人毛骨悚然)的琐事的知识进行评估.不要担心这次采访,这里最好的举动就是走开.

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

  • 这将是有趣的,如果它*也*包含问题的答案. (6认同)
  • 这也是我的观点:`cmp`操作数*对于1和200应该同样快.可能我们*可以想象*愚蠢的实现,而事实并非如此.但是我们可以想象一个*非白痴*实现,其中`while(1)`比`while(200)`更快?同样地,如果在某个史前时代,唯一可用的实施方式就像那样愚蠢,我们今天应该大惊小怪吗?我不这么认为,这是一个尖尖的老板谈话,而且是一个真正的宝石! (5认同)

ana*_*lyg 79

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

顺便问一下,如果你回答

这两段代码同样快,因为两者都需要无限的时间才能完成

面试官会说

但是while (1)每秒可以进行更多的迭代; 你能解释一下原因吗?(这是废话;再次测试你的信心)

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


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

yyy:
    xor eax, eax
    cmp eax, 1     (or 2, depending on your code)
    je xxx
    jmp yyy
xxx:
    ...
Run Code Online (Sandbox Code Playgroud)

启用优化后:

xxx:
    jmp xxx
Run Code Online (Sandbox Code Playgroud)

所以生成的代码完全相同,至少使用优化编译器.

  • "这两段代码同样快,因为两者都需要无限的时间来完成"让我想到了一些有趣的东西.终止无限循环的唯一方法是由一个带电粒子翻转或硬件失败:将语句从`while(00000001){}`改为`while(00000000){}`.您获得的位数越多,值翻转为false的可能性就越小.可悲的是,2也只有一个真正的位.然而,3将运行更长时间.这也仅适用于并不总是优化它的编译器(VC++). (36认同)
  • "我没有弥补." - 请不要注意正在胡说八道的冰袋.C没有布尔类型(它在stdbool.h中有_Bool,但是它不一样,并且`while`的语义在它之前)并且`while`的操作数不是boolean或_Bool或任何其他特定类型.`while`的操作数可以是*any*表达式... while在0上断开并在非0上继续. (29认同)
  • 这段代码实际上是编译器在我的系统上输出的代码.我没有弥补. (27认同)
  • icepack"while的操作数是布尔型" - 完全废话.你是面试官吗?我建议您在提出此类声明之前熟悉C语言及其标准. (10认同)
  • @ mr5不.为了有点翻转实际上导致这样的事情,你谈论的是数万年的执行时间.仅仅是一个**思想实验.**如果你来自不朽的种族,你可能想要使用-1来防止比特翻转影响你的程序. (8认同)
  • @JonathanDickinson"你可能想要使用-1" - 我会使用一个编译器来优化常量,这几乎就是所有常量. (4认同)
  • 那么,如果这是一种自信测试,那么正确的答案是什么?编码员必须每天测试假设,过于自信会导致灾难. (4认同)

Rya*_*ugh 60

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

1 = 00000001
2 = 00000010
Run Code Online (Sandbox Code Playgroud)

如果"为零?" 算法从数字的右侧开始,并且必须检查每个位直到它达到非零位,while(1) { }循环必须检查每次迭代的两倍于while(2) { }循环.

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

  • 我假设面试官的误解更像是"2是一个需要转换为布尔值的int才能在条件表达式中使用,而1已经是布尔值." (39认同)
  • 如果比较算法从左边开始,那就是另一种方式. (7认同)

Tõn*_*uel 32

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

他们让你陷入冲突.如果这是真的,他们很聪明,问题很好.对于某些行业,例如银行业务,将您的问题发布到Stack Overflow可能是拒绝的原因.

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

  • 它确实很棒,但是 while(2) vs while(1) 显然取自 dilbert 漫画。它不能由头脑正常的人发明(无论如何有人想出 while(2) 作为可能的东西来写?)。如果你的假设是真的,你肯定会给出一个非常独特的问题,你可以用谷歌搜索它。就像“while(0xf00b442) 比 while(1) 慢”一样,银行将如何找到面试者的问题?你认为他们是 NSA 并且可以访问 keycore 吗? (2认同)

Old*_*ank 25

我认为线索可以在"高级经理问"中找到.这个人在成为经理时显然已停止编程,然后他/她花了几年时间成为高级经理.从来没有对编程感兴趣,但从那时起从未写过一条线.所以他的参考文献不是"任何体面的编译器",正如一些答案所提到的那样,而是"这个人在20 - 30年前工作过的编译器".

那时,程序员花费了相当大的时间来尝试各种方法来使代码更快更有效,因为"中央小型机"的CPU时间非常有价值.就像编写编译器的人一样.我猜他的公司当时提供的唯一编译器是根据"经常遇到的可以优化的语句"进行优化的,并在遇到一段时间(1)时采取了一些快捷方式并评估了所有内容否则,包括一段时间(2).有过这样的经历可以解释他的立场和对此的信心.

让你受雇的最佳方法可能就是让高级经理在你顺利领导他接下来的面试主题之前,在"编程的美好时光"上讲课2-3分钟.(好时机在这里很重要 - 太快了,你打断了故事 - 太慢了,你被贴上了一个焦点不足的人).在面试结束时告诉他你对这个话题有更多的了解.


Val*_*adu 19

你应该问他他是如何得出这个结论的.在任何体面的编译器下,两个编译为相同的asm指令.所以,他应该告诉你编译器也要开始.即使这样,你也必须非常了解编译器和平台,甚至做出理论上有根据的猜测.最后,它在实践中并不重要,因为还有其他外部因素,如内存碎片或系统负载,这将影响循环而不是这个细节.

  • @GKFX如果你已经给出了答案并且他们告诉你你错了,你就没有理由不让他们解释原因.如果Anatolyg是正确的并且它是对你自信的考验那么你应该解释为什么你回答你的方式并且问他们一样. (14认同)

oua*_*uah 18

为了这个问题,我应该补充一下,我记得来自C委员会的Doug Gwyn写道,一些没有优化器传递的早期C编译器会在汇编中生成一个测试while(1)(比较for(;;)没有它的).

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

  • 没有优化器通过编译器生成两个测试while(1)while(2)
  • 使用优化器传递编译器被指示优化(使用无条件跳转)所有while(1)因为它们被认为是惯用的.这将留下while(2)测试,因此在两者之间产生性能差异.

我当然会向面试官添加不考虑while(1)while(2)相同的构造是低质量优化的标志,因为这些是等效的构造.


小智 9

对这样一个问题的另一个看法就是看你是否有勇气告诉你的经理他/她错了!你可以多么轻柔地沟通它.

我的第一直觉是生成程序集输出以向管理员显示任何体面的编译器应该处理它,如果它没有这样做,你将为它提交下一个补丁:)


Bar*_*jen 8

为了看到这么多人深入研究这个问题,请说明为什么这很可能是一个测试,看看你想要多快地微观优化事物.

我的回答是; 这并不重要,我更关注我们正在解决的业务问题.毕竟,这就是我要付出的代价.

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

现在去写一些代码.;-)


pac*_*low 5

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

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


Rok*_*alj 5

也许面试官故意问这么蠢的问题,就是想让你说出3点:

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


Tom*_*ner 5

如果您担心优化,则应使用

for (;;)
Run Code Online (Sandbox Code Playgroud)

因为那没有测试。(讽刺模式)

  • 这就是为什么使用while(2)明智的做法,它留出了优化的空间,然后在准备提高性能时只需切换到for(;;)。 (2认同)

归档时间:

查看次数:

87062 次

最近记录:

7 年,5 月 前