哪个更快:while(1)while while(2)?
这是一位高级经理提出的面试问题。
哪个更快?
while(1) { // Some code }
要么
while(2) { //Some code }
我说过,两者执行速度一样,里面的expression式最终应该是true
还是false
。 在这种情况下,两者评估为true
并且在while
条件内没有额外的条件指令。 所以,两者都会有相同的执行速度,我更喜欢while(1)。
但面试官自信地说:“ while(1)
比while(2)
快。” (他没有testing我的信心)
这是真的?
另请参见: “for(;;)”比“while(TRUE)”更快? 如果没有,为什么人们使用它?
两个循环都是无限的,但是我们可以看到哪个循环每次迭代需要更多的指令/资源。
使用gcc,我编译了以下两个程序来进行不同级别的优化:
int main(void) { while(1) {} return 0; }
int main(void) { while(2) {} return 0; }
即使没有优化( -O0
), 生成的程序集也是相同的。 所以两个回路之间没有速度差。
作为参考,这里是生成的程序集(使用gcc main.c -S -masm=intel
带有优化标志的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
及以上,它们都产生与前面的例子相同的组件(无条件的jmp
返回到前面的标签)。
TL; DR
在GCC下,不同的循环被编译成相同的程序集。 编译器评估常量值,不会执行任何实际比较。
故事的寓意是:
- C ++源代码和CPU指令之间存在一层转换,这一层对性能有重要的影响。
- 因此,只能看源代码才能评估性能。
- 编译器应该足够聪明,以优化这种微不足道的情况。 程序员不应该在绝大多数情况下浪费时间来思考它们。
是的, while(1)
比while(2)
快得多, 对于一个人来说阅读! 如果我while(1)
不熟悉的代码库中看到,我立即知道作者的意图,我的眼球可以继续下一行。
如果我看到while(2)
,我可能会停下脚步,试图弄明白为什么作者没有写while(1)
。 作者的手指滑落在键盘上了吗? 这个代码库的维护者使用while(n)
作为一个难以理解的注释机制,使循环看起来不同? 对于一些破损的静态分析工具中的虚假警告,这是一个粗略的解决方法吗? 或者这是一个线索,我正在阅读生成的代码? 这是一个错误的发现和replace – 所有,或一个错误的合并,或宇宙射线? 也许这行代码应该做一些非常不同的事情。 也许它应该while(w)
或while(x2)
读取。 我最好在文件的历史中find作者,并发送给他们一个“WTF”电子邮件……现在我已经打破了我的心理背景。 while(2)
可能会消耗我几分钟的时间, while(1)
会花费几分之一秒!
我夸大,但只是一点点。 代码可读性非常重要。 这在采访中值得一提!
现有答案显示特定编译器为特定目标提供的特定编译器生成的代码并不能完全回答这个问题,除非在特定的上下文中提出了这个问题(“对于x86_64,使用gcc 4.7.2更快与默认选项?“,例如)。
就语言定义而言, 抽象机中 while (1)
评估整数常量1
, while (2)
评估整数常数2
; 在这两种情况下,比较结果的等于零。 语言标准对这两个构念的相对performance完全没有提及。
我可以想象,一个非常天真的编译器可能为这两种forms生成不同的机器代码,至less在编译时没有请求优化。
另一方面,C编译器绝对必须在编译时估计一些常量expression式,当它们出现在需要不断expression的上下文中时。 例如,这个:
int n = 4; switch (n) { case 2+2: break; case 4: break; }
需要诊断; 一个懒惰的编译器没有延迟评估2+2
的选项,直到执行时间。 由于编译器必须能够在编译时计算常量expression式,所以没有充分的理由不利用该function,即使不是必需的。
C标准( N1570 6.8.5p4)说
迭代语句会导致一个被称为循环体的语句被重复执行,直到控制expression式比较等于0。
所以相关的常量expression式是1 == 0
和2 == 0
,二者的值都是int
值0
。 (这些比较隐含在while
循环的语义中;它们不作为实际的Cexpression式存在。)
天真的编译器可以为这两个构造生成不同的代码。 例如,对于第一个,它可以生成一个无条件的无限循环(将1
视为一个特殊情况),第二个循环可以生成等价于2 != 0
的显式运行时比较。 但是我从来没有遇到一个实际上会这样的C编译器,而且我非常怀疑这样的编译器是否存在。
大多数编译器(我很想说所有的产品质量编译器)都有select来请求额外的优化。 在这种select下,任何编译器都不会为这两种forms生成不同的代码。
如果你的编译器为这两个结构生成不同的代码,首先检查不同的代码序列是否有不同的性能。 如果他们这样做,尝试再次编译优化选项(如果可用)。 如果仍然不同,请向编译器供应商提交错误报告。 不符合C标准的意义上的(不一定)是一个错误,但几乎肯定是一个应该纠正的问题。
底线: while (1)
和while(2)
几乎肯定有相同的performance。 它们具有完全相同的语义,没有任何编译器不会生成相同的代码。
虽然编译器为while(1)
生成更快的代码比while(2)
更合适,但编译器生成while(1)
代码速度要快于while(1)
中的另一个while(1)
同样的节目。
(你问的问题还隐含着另外一个问题:你如何处理一个坚持不正确的技术点的面试者,这对于Workplace网站可能是个好问题)。
等一下。 面试官,他看起来像这个人吗?
面试官本人面试失败已经够糟糕了,如果这家公司的其他程序员已经“通过”了这个testing呢?
不。评估语句1 == 0
和2 == 0
应该同样快。 我们可以想象可怜的编译器实现,其中一个可能比另一个快。 但是为什么一个人要比另一个快呢?
即使有些晦涩的情况下,声称是真实的,程序员不应该根据晦涩的知识(在这种情况下,令人毛骨悚然的)琐事来评估。 不要担心这个采访,这里最好的举措就是走开。
免责声明:这不是一个原始的Dilbert卡通。 这只是一个混搭 。
你的解释是正确的。 这似乎是一个除了技术知识之外还考验你自信心的问题。
顺便说一句,如果你回答
这两个代码块都是同样快的,因为两者都需要无限的时间才能完成
面试官会说
但
while (1)
可以每秒做更多的迭代; 你能解释为什么吗? (这是无稽之谈;再次testing你的信心)
所以通过像你这样回答,你节省了一些时间,否则你会讨论这个坏问题。
以下是我的系统上的编译器生成的示例代码(MS Visual Studio 2012),closures了优化:
yyy: xor eax, eax cmp eax, 1 (or 2, depending on your code) je xxx jmp yyy xxx: ...
优化开启后:
xxx: jmp xxx
所以生成的代码是完全一样的,至less有一个优化的编译器。
这个问题最可能的解释是面试官认为处理器逐个检查数字的各个位,直到它达到一个非零值:
1 = 00000001 2 = 00000010
如果“是零?” algorithm从数字的右侧开始,并且必须检查每个比特,直到达到非零比特,则while(1) { }
循环将不得不检查每次迭代两倍的比特数。循环。
这就要求计算机工作的思维模式非常错误,但它确实有自己的内在逻辑。 一种检查方法是询问while(-1) { }
或while(3) { }
是否同样快,或者while(32) { }
会更慢 。
当然,我不知道这位经理的真正意图,但我提出了一个完全不同的观点:当一个新成员join一个团队时,了解他如何对冲突情况作出反应是有用的。
他们让你陷入冲突。 如果这是真的,他们是聪明的,问题是好的。 对于某些行业,比如银行业,将问题发布到Stack Overflow可能是拒绝的原因。
但是我当然不知道,我只是提出一个select。
我想这个线索可以在“高级经理问”中find。 这个人在成为经理人的时候显然停止了编程,然后花了几年时间成为一名高级经理。 从来没有失去对编程的兴趣,但从那时起从来没有写过一行。 所以他的提法并不是“有任何像样的编译器”,而是“这个人20-30年前的编译器”。
当时,程序员花费了相当多的时间尝试各种方法来使代码更快更高效,因为“中央小型机”的CPU时间非常有价值。 编写编译器的人也一样。 我猜测他公司当时提供的唯一编译器是在“经常遇到的可以优化的语句”的基础上进行了优化,并在遇到一段时间后采取了一些捷径(1),并评估了一切其他,包括一段时间(2)。 有了这样的经验可以解释他的立场和他的信心。
聘请你最好的方法可能是让高级pipe理人员在你顺利地引导他走向下一个面试主题之前,在“编程的美好时光”上讲2-3分钟。 (好的时机在这里很重要 – 太快了,你打断了这个故事 – 太慢了,你被贴上了一个焦点不足的标签)。 在面试结束时告诉他,你会对这个话题有更多的兴趣。
你应该问他他是如何得出这个结论的。 在那里的任何体面的编译器,这两个编译到相同的asm指令。 所以,他也应该告诉你编译器也要开始。 即使如此,你也必须非常熟悉编译器和平台,甚至可以进行理论上的猜测。 最后,在实践中并不重要,因为还有其他外部因素,比如内存碎片或者系统负载,会比这个细节更多地影响循环。
为了这个问题,我应该补充一点,我记得C委员会的Doug Gwyn写道,一些没有优化器通过的早期的C编译器会在while(1)
产生一个汇编的testing(比较for(;;)
没有它)。
我会通过提供这个历史logging来回答面试官,然后说,即使我会非常惊讶的任何编译器做到这一点,编译器可以有:
- 没有优化器通过编译器生成一个testing
while(1)
while(2)
-
while(1)
因为它们被认为是惯用的。 这将使得while(2)
在testing中离开,因此两者之间的性能差异。
我当然会向面试官补充:不考虑while(1)
和while(2)
同样的构造是低质量优化的标志,因为这些是等价的构造。
另外一个问题就是看看你是否有勇气告诉你的经理他/她是错的! 你可以轻松地传达它。
我的第一本能就是生成汇编输出,以向pipe理员展示任何体面的编译器应该照顾它,如果不这样做,你将提交下一个补丁:)
要看到这么多的人深入研究这个问题,显示了为什么这可能是一个testing,看看如何快速地微观优化的东西。
我的答案是; 这并不重要,我更关注我们正在解决的业务问题。 毕竟,这就是我要付出的代价。
而且,我会selectwhile(1) {}
因为它更常见,而其他队友不需要花时间来弄清楚为什么有人会select比1更高的数字。
现在去写一些代码。 😉
当这种胡说可能有所作为时,我曾经编程C和汇编代码。 当它确实有所作为的时候,我们在大会上写下了它。
如果我被问到这个问题,我会重复唐纳德·克努特(Donald Knuth)着名的关于过早优化的1974年的引用,如果面试官不笑,继续走下去。
在我看来,这是被掩盖为技术性问题的行为面试问题之一。 有些公司会这样做 – 他们会问一个技术性的问题,让任何有能力的程序员都能相当容易地回答,但是当受访者给出正确的答案时,采访者会告诉他们他们错了。
公司希望看到你将如何在这种情况下作出反应。 你是否安静地坐在那里,不要因为自我怀疑或害怕面试官的不安,而把你的回答是正确的? 或者你是否愿意挑战一个你认为是错误的权威人士呢? 他们想看看你是否愿意坚持信念,如果你能够以一种机智和尊重的方式去做。
这里有一个问题:如果你真的编写一个程序并测量它的速度,两个循环的速度可能会不同! 为了一些合理的比较:
unsigned long i = 0; while (1) { if (++i == 1000000000) break; } unsigned long i = 0; while (2) { if (++i == 1000000000) break; }
with some code added that prints the time, some random effect like how the loop is positioned within one or two cache lines could make a difference. One loop might by pure chance be completely within one cache line, or at the start of a cache line, or it might to straddle two cache lines. And as a result, whatever the interviewer claims is fastest might actually be fastest – by coincidence.
Worst case scenario: An optimising compiler doesn't figure out what the loop does, but figures out that the values produced when the second loop is executed are the same ones as produced by the first one. And generate full code for the first loop, but not for the second.
They are both equal – the same.
According to the specifications anything that is not 0 is considered true, so even without any optimization, and a good compiler will not generate any code for while(1) or while(2). The compiler would generate a simple check for != 0
.
Judging by the amount of time and effort people have spent testing, proving, and answering this very straight forward question Id say that both were made very slow by asking the question.
And so to spend even more time on it …
"while (2)" is ridiculous, because,
"while (1)", and "while (true)" are historically used to make an infinite loop which expects "break" to be called at some stage inside the loop based upon a condition that will certainly occur.
The "1" is simply there to always evaluate to true and therefore, to say "while (2)" is about as silly as saying "while (1 + 1 == 2)" which will also evaluate to true.
And if you want to be completely silly just use: –
while (1 + 5 - 2 - (1 * 3) == 0.5 - 4 + ((9 * 2) / 4)) { if (succeed()) break; }
Id like to think your coder made a typo which did not effect the running of the code, but if he intentionally used the "2" just to be weird then sack him before he puts weird sh!t all through your code making it difficult to read and work with.
Maybe the interviewer posed such dumb question intentionally and wanted you to make 3 points:
- Basic reasoning. Both loops are infinite, it's hard to talk about performance.
- Knowledge about optimisation levels. He wanted to hear from you if you let the compiler do any optimisation for you, it would optimise the condition, especially if the block was not empty.
- Knowledge about microprocessor architecture. Most architectures have a special CPU instruction for comparision with 0 (while not necessarily faster).
That depends on the compiler.
If it optimizes the code, or if it evaluates 1 and 2 to true with the same number of instructions for a particular instruction set, the execution speed will be the same.
In real cases it will always be equally fast, but it would be possible to imagine a particular compiler and a particular system when this would be evaluated differently.
I mean: this is not really a language (C) related question.
If you're that worried about optimisation, you should use
for (;;)
because that has no tests. (cynic mode)
The only reason I can think of why the while(2)
would be any slower is:
-
The code optimizes the loop to
cmp eax, 2
-
When the subtract occurs you're essentially subtracting
一个。
00000000 - 00000010 cmp eax, 2
代替
湾
00000000 - 00000001 cmp eax, 1
cmp
only sets flags and does not set a result. So on the least significant bits we know if we need to borrow or not with b . Whereas with a you have to perform two subtractions before you get a borrow.