这是一位高级经理提出的面试问题。
哪个更快?
while(1) {
// Some code
}
或
while(2) {
//Some code
}
我说两者都具有相同的执行速度,因为while
中的表达式应最终评估为true
或false
。在这种情况下,两者都评估为true
并且while
条件中没有额外的条件指令。因此,两者都具有相同的执行速度,而我更喜欢(1)。
但是采访者自信地说:
“检查您的基础知识。while(1)
比while(2)
快。”
(他没有考验我的信心)
这是真的吗?
答案 0 :(得分:669)
两个循环都是无限的,但是我们可以看到哪一个循环每次迭代需要更多的指令/资源。
使用gcc,我将以下两个程序编译为不同优化级别的程序集:
int main(void) {
while(1) {}
return 0;
}
int main(void) {
while(2) {}
return 0;
}
即使没有优化(-O0
), the generated assembly was identical for both programs。因此,两个循环之间没有速度差异。
作为参考,这里是生成的程序集(使用带有优化标志的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()) {}
在-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
回到前一个标签)。
在GCC下,不同的循环被编译为相同的程序集。编译器评估常量值,并且不打算执行任何实际比较。
故事的寓意是:
答案 1 :(得分:269)
是的,while(1)
要快于while(2)
,让人类阅读!如果我在一个不熟悉的代码库中看到while(1)
,我立即知道是什么作者打算,我的眼球可以继续下一行。
如果我看到while(2)
,我可能会停下来试图找出作者没有写while(1)
的原因。作者的手指在键盘上滑动了吗?这个代码库的维护者是否使用while(n)
作为一种模糊的注释机制来使循环看起来不同?对于某些破碎的静态分析工具中的虚假警告,这是一个粗略的解决方法吗?或者这是我正在阅读生成代码的线索吗?这是一个由于不明智的查找和替换全部,或者是一个糟糕的合并或一个宇宙射线导致的错误?也许这行代码应该做一些截然不同的事情。也许应该阅读while(w)
或while(x2)
。我最好在文件的历史中找到作者并向他们发送“WTF”电子邮件......现在我已经打破了我的心理背景。 while(2)
可能需要几分钟的时间,而while(1)
只需要几分之一秒!
我夸张,但只是一点点。代码可读性非常重要。这在接受采访时值得一提!
答案 2 :(得分:151)
现有答案显示特定编译器为具有特定选项集的特定目标生成的代码不能完全回答问题 - 除非在该特定上下文中询问了问题(“使用gcc 4.7更快。” 2用于x86_64,默认选项?“,例如)。
就语言定义而言,在抽象机器中 while (1)
评估整数常量1
,while (2)
计算整数常量{{ 1}};在两种情况下,结果都将相等性与零进行比较。语言标准对两种结构的相对性能完全没有说明。
我可以想象一个非常天真的编译器可能会为这两种形式生成不同的机器代码,至少在编译时没有请求优化。
另一方面,C编译器必须在编译时评估某些常量表达式,当它们出现在需要常量表达式的上下文中时。例如,这个:
2
需要诊断;懒惰的编译器没有选择将int n = 4;
switch (n) {
case 2+2: break;
case 4: break;
}
的评估推迟到执行时间。由于编译器必须能够在编译时评估常量表达式,因此即使不需要,也没有充分的理由不利用该功能。
C标准(N1570 6.8.5p4)表示
迭代语句会导致一个名为循环体的语句 反复执行,直到控制表达式比较等于 0
因此,相关的常量表达式为2+2
和1 == 0
,两者都评估为2 == 0
值int
。 (这些比较隐含在0
循环的语义中;它们不作为实际的C表达式存在。)
一个反常天真的编译器可以为这两个结构生成不同的代码。例如,对于第一个,它可以生成无条件无限循环(将while
视为特殊情况),对于第二个,它可以生成等效于1
的显式运行时比较。但是我从来没有遇到过一个实际上就是这样的C编译器,我很怀疑这样的编译器是否存在。
大多数编译器(我很想说所有生产质量编译器)都可以选择进行额外的优化。在这样的选项下,任何编译器都不太可能为这两种形式生成不同的代码。
如果您的编译器为这两个构造生成不同的代码,请首先检查不同的代码序列是否实际上具有不同的性能。如果是,请尝试使用优化选项再次编译(如果可用)。如果它们仍然不同,请向编译器供应商提交错误报告。它不是(必然)一个不符合C标准的错误,但它几乎肯定是一个应该纠正的问题。
底线:2 != 0
和while (1)
几乎肯定具有相同的效果。它们具有完全相同的语义,并且任何编译器都没有充分的理由不生成相同的代码。
虽然编译器为while(2)
生成比while(1)
更快的代码是完全合法的,但编译器为while(2)
生成更快的代码比同时生成更快的代码同样合法。 while(1)
在同一个程序中。
(还有一个问题隐含在你问的那个问题中:你如何处理一个坚持不正确的技术要点的面试官。这可能是the Workplace site)的一个好问题。
答案 3 :(得分:137)
等一下。面试官,他看起来像这个人吗?
面试官本人失败这次采访已经够糟糕了, 如果这家公司的其他程序员已经通过&#34;这个测试?
没有。评估语句1 == 0
和2 == 0
应该同样快。我们可以想象糟糕的编译器实现,其中一个可能比另一个更快。但是,为什么一个人应该比另一个人更快,没有好的理由。
即使在声明属实的情况下存在一些模糊的情况,也不应该根据对模糊(在这种情况下,令人毛骨悚然)的琐事的知识来评估程序员。不要担心这次采访,这里最好的举动就是离开。
免责声明: 这不是原创的Dilbert漫画。这只是mashup。
答案 4 :(得分:78)
你的解释是正确的。除了技术知识之外,这似乎是一个测试你自信心的问题。
顺便说一句,如果你回答了
面试官会说这两段代码同样快,因为两者都需要无限的时间来完成
但是
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
所以生成的代码完全相同,至少使用优化编译器。
答案 5 :(得分:59)
该问题最可能的解释是,面试官认为处理器逐个检查数字的各个位,直到它达到非零值:
1 = 00000001
2 = 00000010
如果&#34;为零?&#34;算法从数字的右侧开始,并且必须检查每个位直到它达到非零位,while(1) { }
循环必须检查每次迭代的次数是while(2) { }
循环的两倍。
这需要一个关于计算机如何工作的非常错误的心理模型,但它确实有自己的内部逻辑。一种检查方法是询问while(-1) { }
或while(3) { }
是否同样快,或者while(32) { }
更慢。
答案 6 :(得分:32)
当然我不知道这位经理的真实意图,但我提出了一个完全不同的观点:当一个新成员加入一个团队时,知道他如何应对冲突局势是有用的。
他们让你陷入冲突。如果这是真的,他们很聪明,问题很好。对于某些行业,例如银行业务,将您的问题发布到Stack Overflow可能是拒绝的原因。
但我当然不知道,我只提出一个选择。
答案 7 :(得分:25)
我认为线索可以在“高级经理询问”中找到。这个人在成为经理时显然已停止编程,然后他/她花了几年时间成为高级经理。从来没有对编程感兴趣,但从那时起从未写过一条线。所以他的参考文献不是“任何体面的编译器”,正如一些答案所提到的那样,而是“这个人在20 - 30年前工作的编译器”。
当时,程序员花费了相当大的时间来尝试各种方法来使代码更快更有效,因为“中央小型机”的CPU时间非常有价值。就像编写编译器的人一样。我猜他的公司当时提供的唯一编译器是根据“经常遇到的可以优化的语句”进行优化的,并在遇到一段时间(1)时采取了一些快捷方式并评估了所有内容否则,包括一段时间(2)。有过这样的经历可以解释他的立场和对此的信心。
让你受雇的最佳方法可能就是让你的高级经理能够被带走,并在“好”顺利领导他之前,在“节目的美好时光”上讲2-3分钟面向下一个面试主题。 (好时机在这里很重要 - 太快了,你打断了故事 - 太慢了,你被贴上了一个焦点不足的人)。在面试结束时告诉他你对这个话题有更多的了解。
答案 8 :(得分:19)
你应该问他是怎么得出这个结论的。在任何体面的编译器下,两个编译为相同的asm指令。所以,他应该告诉你编译器也要开始。即使这样,你也必须非常了解编译器和平台,甚至做出理论上有根据的猜测。最后,它在实践中并不重要,因为还有其他外部因素,如内存碎片或系统负载,这些因素会影响循环,而不是这个细节。
答案 9 :(得分:18)
为了这个问题,我应该补充一下,我记得来自C委员会的Doug Gwyn写道,一些没有优化器传递的早期C编译器会在while(1)
的汇编中生成一个测试(与{{1相比)哪个没有它。)
我会通过给出这个历史记录回答面试官,然后说即使我对任何编译器都这样做感到非常惊讶,编译器也可以:
for(;;)
和while(1)
while(2)
,因为它们被认为是惯用的。这会使while(1)
进行测试,从而在两者之间产生性能差异。我当然会向面试官补充说,不考虑while(2)
和while(1)
相同的构造是低质量优化的标志,因为这些是等效构造。
答案 10 :(得分:9)
对这个问题的另一种看法是看你是否有勇气告诉你的经理他/她错了!你可以多么轻柔地沟通它。
我的第一直觉是生成程序集输出以向管理员显示任何体面的编译器应该处理它,如果它没有这样做,你将为它提交下一个补丁:)
答案 11 :(得分:8)
要看到这么多人深入研究这个问题,请准确说明为什么这可以很好地测试你想要微优化事物的速度。
我的回答是;它并不重要,我更关注我们正在解决的业务问题。毕竟,这就是我要付出的代价。
此外,我会选择while(1) {}
,因为它更常见,其他队友也不需要花时间弄清楚为什么有人会选择高于1的数字。
现在去写一些代码。 ; - )
答案 12 :(得分:4)
在我看来,这是一个被掩盖为技术问题的行为面试问题。有些公司会这样做 - 他们会问一个技术问题,任何有能力的程序员都应该很容易回答,但当受访者给出正确的答案时,面试官会告诉他们他们错了。
该公司希望了解您将如何应对这种情况。你是否安静地坐在那里,不要因为自我怀疑或害怕面试官而烦恼你的答案是否正确?或者你是否愿意挑战一个你知道错误的权威人士?他们想看看你是否愿意为自己的信念而坚持,如果你能以一种委婉和尊重的方式做到这一点。
答案 13 :(得分:4)
如果你担心优化,你应该使用
for (;;)
因为没有测试。 (愤世嫉俗的模式)
答案 14 :(得分:3)
当这种废话可能产生影响时,我曾经编程C和汇编代码。当它确实有所作为时,我们在大会上写了它。
如果我被问到这个问题,我会重复唐纳德·克努特(Donald Knuth)1974年关于过早优化的着名言论,如果采访者不笑并继续前进,那就走了。
答案 15 :(得分:3)
也许面试官故意提出这样一个愚蠢的问题,并希望你提出3分:
答案 16 :(得分:2)
他们都是平等的 - 相同。
根据规范,任何非0的都被认为是真的,所以即使没有任何优化,一个好的编译器也不会产生任何代码
for while(1)或while(2)。编译器将生成!= 0
的简单检查。
答案 17 :(得分:2)
这是一个问题:如果您实际编写程序并测量其速度,两个循环的速度可能会有所不同!进行一些合理的比较:
unsigned long i = 0;
while (1) { if (++i == 1000000000) break; }
unsigned long i = 0;
while (2) { if (++i == 1000000000) break; }
添加了一些打印时间的代码,一些随机效果,如循环在一个或两个缓存行中的位置可能会有所不同。一个循环可能完全在一个缓存行内,或者在缓存行的开头,或者它可能跨越两个缓存行。因此,无论采访者声称最快,实际上可能是最快的 - 巧合。
最糟糕的情况:优化编译器不会弄清楚循环的作用,但会发现第二个循环执行时产生的值与第一个循环产生的值相同。并为第一个循环生成完整代码,但不为第二个循环生成完整代码。
答案 18 :(得分:2)
根据人们花费大量时间和精力来测试,证明和回答这个非常直接的问题.Id说,通过提出这个问题,两者都变得非常缓慢。
所以要花更多的时间在上面......
“而(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)) {
if (succeed())
break;
}
我想你的编码器写了一个不会影响代码运行的拼写错误,但是如果他故意使用“2”只是为了变得怪异,那么在他把你的代码制作完全搞砸之前解雇他它很难阅读和使用。
答案 19 :(得分:1)
这取决于编译器。
如果它优化了代码,或者如果用特定指令集的相同指令数将1和2计算为真,则执行速度将相同。
在实际情况下,它总是同样快,但是可以想象一个特定的编译器和一个特定的系统,当这将被不同地评估。
我的意思是:这不是一个与语言(C)相关的问题。
答案 20 :(得分:0)
由于想要回答这个问题的人想要最快的循环,我会回答说两者同样编译成相同的汇编代码,如其他答案中所述。不过你可以使用'循环展开给面试官建议; a {while while循环而不是while循环。
谨慎:您需要确保循环至少始终运行一次。
循环中应该有一个中断条件。
对于那种循环,我个人更喜欢使用do {} while(42),因为除了0之外的任何整数都可以完成这项工作。
答案 21 :(得分:0)
最明显的答案是:如前所述,两个片段都将运行同样繁忙的无限循环,这会使程序无限慢 。
尽管将C关键字重新定义为宏在技术上将具有未定义的行为,但这是我想到的使所有代码片段都变得快速的唯一方法:您可以在2个片段上方添加以下行:
#define while(x) sleep(x);
确实会使while(1)
的速度是while(2)
的两倍(或一半)。
答案 22 :(得分:-4)
我能想到while(2)
为什么会变慢的唯一原因是:
代码将循环优化为
cmp eax, 2
当减法发生时,你基本上减去
一个。 00000000 - 00000010 cmp eax, 2
而不是
湾00000000 - 00000001 cmp eax, 1
cmp
仅设置标志,不设置结果。因此,在最不重要的位上,我们知道是否需要借用 b 来借用。而对于 a ,您必须在借入之前执行两次减法。