这是一位高级经理问的面试问题。
哪个更快?
while(1) {
// Some code
}
或者
while(2) {
//Some code
}
我说过两者具有相同的执行速度,因为 while
中的表达式最终应该计算为 true
或 false
。在这种情况下,两者都计算为 true
,并且 while
条件内没有额外的条件指令。因此,两者都将具有相同的执行速度,我更喜欢 while (1)。
但面试官自信地说:“检查你的基础。while(1)
比while(2)
快。” (他不是在考验我的信心)
这是真的?
另请参阅:“for(;;)”是否比“while (TRUE)”快?如果不是,人们为什么要使用它?
0x100000f90: jmp 0x100000f90
(地址不同,显然)。面试官可能在注册测试与简单的标记跳跃之间对冲。这个问题和他们的假设都是蹩脚的。
两个循环都是无限的,但我们可以看到哪一个循环每次迭代需要更多的指令/资源。
使用 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
返回到前面的标签)。
TL;博士
在 GCC 下,不同的循环被编译成相同的程序集。编译器评估常量值并且不打扰执行任何实际比较。
这个故事的寓意是:
C++ 源代码和 CPU 指令之间存在一层转换,这一层对性能有重要影响。
因此,不能仅通过查看源代码来评估性能。
编译器应该足够聪明以优化这些琐碎的情况。在绝大多数情况下,程序员不应该浪费时间去思考它们。
是的,while(1)
比 while(2)
快得多,人类可以阅读!如果我在不熟悉的代码库中看到 while(1)
,我会立即知道作者的意图,并且我的眼球可以继续到下一行。
如果我看到 while(2)
,我可能会停下来,试图找出作者为什么不写 while(1)
。作者的手指在键盘上打滑了吗?这个代码库的维护者是否使用 while(n)
作为一种晦涩的注释机制来使循环看起来不同?对于某些损坏的静态分析工具中的虚假警告,这是一种粗略的解决方法吗?或者这是我正在阅读生成的代码的线索?它是由不明智的查找并替换所有、错误的合并或宇宙射线导致的错误吗?也许这行代码应该做一些截然不同的事情。也许它应该是 while(w)
或 while(x2)
。我最好在文件的历史记录中找到作者,然后给他们发一封“WTF”电子邮件……现在我已经打破了我的心理背景。 while(2)
可能会占用我几分钟的时间,而 while(1)
可能只需要几分之一秒!
我夸大了,但只是一点点。代码可读性真的很重要。这在采访中值得一提!
svn-annotate
或 git blame
(或其他),并且通常需要几分钟来加载文件责备历史记录。然后终于下定决心“啊我明白了,作者高中刚毕业时写的这句话”,就浪费了10分钟……
1
。
while(2)
时,我经历了与此答案中描述的完全相同的心理过程(考虑到“此代码库的维护者是否使用 while(n) 作为一种晦涩的注释机制来使循环看起来不同的?”)。所以不,你没有夸大其词!
显示特定编译器为具有特定选项集的特定目标生成的代码的现有答案并不能完全回答问题 - 除非在特定上下文中提出问题(“使用 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;
}
需要诊断;惰性编译器没有将 2+2
的评估推迟到执行时间的选项。由于编译器必须具有在编译时评估常量表达式的能力,因此没有充分的理由不利用这种能力,即使它不是必需的。
C 标准 (N1570 6.8.5p4) 说
迭代语句会导致重复执行称为循环体的语句,直到控制表达式比较等于 0。
因此相关的常量表达式是 1 == 0
和 2 == 0
,它们的计算结果都是 int
值 0
。 (这些比较隐含在 while
循环的语义中;它们不作为实际的 C 表达式存在。)
一个天真的编译器可能为这两种结构生成不同的代码。例如,对于第一个,它可以生成一个无条件的无限循环(将 1
视为一种特殊情况),对于第二个,它可以生成一个等效于 2 != 0
的显式运行时比较。但是我从来没有遇到过实际上会以这种方式运行的 C 编译器,而且我严重怀疑是否存在这样的编译器。
大多数编译器(我想说所有生产质量的编译器)都有请求额外优化的选项。在这样的选择下,任何编译器都不太可能为这两种形式生成不同的代码。
如果您的编译器为这两个结构生成不同的代码,首先检查不同的代码序列是否实际上具有不同的性能。如果是这样,请尝试使用优化选项(如果可用)再次编译。如果它们仍然不同,请向编译器供应商提交错误报告。从未能符合 C 标准的意义上说,这不是(必然)一个错误,但几乎可以肯定它是一个应该纠正的问题。
底线:while (1)
和 while(2)
几乎当然具有相同的性能。它们具有完全相同的语义,任何编译器都没有充分的理由不生成相同的代码。
尽管编译器为 while(1)
生成比 while(2)
更快的代码是完全合法的,但编译器为 while(1)
生成比在同一程序中再次出现 while(1)
更快的代码同样合法。
(您问的问题中隐含了另一个问题:您如何处理坚持不正确技术观点的面试官。这可能是 the Workplace site 的一个好问题)。
while
的控制表达式必须是标量类型,并且循环体重复直到表达式比较等于 0。它并没有说有一个隐式 expr != 0
被评估......这将需要将结果 - 0 或 1 - 依次与 0 进行比较,无穷无尽。不,表达式与 0 进行比较,但该比较不会产生值。 PS我投了赞成票。
<expr> == 0
;这就是 C 中“比较等于 0”意味着 的含义。这种比较是 while
循环语义的一部分。无论是在标准中还是在我的答案中,都没有暗示需要再次将结果与 0
进行比较。 (我应该写 ==
而不是 !=
。)但是我的那部分答案不清楚,我会更新它。
while (2)
、while (pointer)
、while (9.67)
生成的代码,您将看不到任何生成 0
或 1
的代码。 “我应该写 == 而不是 !=" - 不,这没有任何意义。
... == 0
C 表达式。我的观点是,标准对 while
循环的描述所要求的“比较等于 0”和明确的 x == 0
表达式在逻辑上都暗示了相同的操作。而且我认为一个痛苦天真的 C 编译器可能会为任何 while
循环生成 int
值 0
或 1
的代码——尽管我不相信任何实际的编译器就是这么幼稚。
等一下。面试官,他长得像这个人吗?
https://i.stack.imgur.com/L6P1H.png
面试官自己这次面试失败已经够惨了,万一这家公司的其他程序员都“通过”了这个测试呢?
不会。评估语句 1 == 0
和 2 == 0
应该同样快。我们可以想象糟糕的编译器实现,其中一个可能比另一个更快。但是没有好的理由为什么一个应该比另一个更快。
即使在某些晦涩的情况下,该声明是正确的,也不应根据对晦涩(在这种情况下,令人毛骨悚然)琐事的知识来评估程序员。不用担心这次采访,这里最好的举动是走开。
免责声明:这不是原版呆伯特卡通。这只是一个 mashup。
cmp
操作数的运行周期是否比 2 到 0 少?一般执行 cmp 需要多少个周期?它是根据位模式变化的吗?它们是否更“复杂”的位模式会减慢 cmp
?我个人不知道。你可以想象一个超级白痴的实现从 0 到 n 逐位检查(例如 n=31)。
cmp
操作数应该同样快。也许我们可以想象愚蠢的实现,但情况并非如此。但是我们能想象一个 非白痴 实现,其中 while(1)
比 while(200)
快吗?同样,如果在某个史前时代,唯一可用的实现就是这样的愚蠢,我们今天应该对此大惊小怪吗?我不这么认为,这是尖头的老板谈话,而且是真正的宝石!
你的解释是正确的。这似乎是一个考验你除了技术知识之外的自信心的问题。
顺便说一句,如果你回答
两段代码同样快,因为两者都需要无限时间才能完成
面试官会说
但是虽然(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
所以生成的代码是完全一样的,至少使用优化编译器是一样的。
while
的语义早在它之前)并且 while
的操作数不是布尔型或 _Bool 或任何其他特定类型. while
的操作数可以是 any 表达式 ... while 在 0 处中断并在非 0 处继续。
while (00000001) {}
更改为 while (00000000) {}
。真实位越多,值翻转为假的机会就越小。可悲的是,2 也只有一个真正的位。但是,3 将运行更长的时间。这也仅适用于并不总是优化它的编译器(VC++)。
对这个问题最可能的解释是,面试官认为处理器会逐位检查数字的各个位,直到达到非零值:
1 = 00000001
2 = 00000010
如果“为零?”算法从数字的右侧开始,并且必须检查每个位,直到它到达一个非零位,while(1) { }
循环每次迭代必须检查的位是 while(2) { }
循环的两倍。
这需要一个非常错误的关于计算机如何工作的心智模型,但它确实有自己的内部逻辑。一种检查方法是询问 while(-1) { }
或 while(3) { }
是否会同样快,或者 while(32) { }
是否会更慢。
cmp
算法是线性位检查,在第一个差异时提前退出循环。
-O0
输出不够字面意思。”我从来没有说过这样的话,我根本没有 gcc 或 clang 的想法。你一定是误读了我。我只是不认为 MSVC 产生的东西很有趣。
while(1)
上的断点不会在循环之前中断,而是在表达式上。但是当优化表达式时,它仍然会在循环内崩溃。以 this code 多休息。在未优化的调试模式下,您会得到 this。优化时,many breakpoints fall together or even spill over into the next function(IDE 在调试时显示结果断点)- corresponding disassembly。
当然,我不知道这位经理的真实意图,但我提出了一个完全不同的观点:在雇用新成员进入团队时,了解他对冲突情况的反应很有用。
他们让你陷入冲突。如果这是真的,他们很聪明,问题也很好。对于某些行业,例如银行业,将您的问题发布到 Stack Overflow 可能是拒绝的原因。
但我当然不知道,我只是提出一种选择。
我想线索可以在“一位高级经理的询问”中找到。这个人在当了经理后显然就停止了编程,然后他/她花了几年时间才成为高级经理。从未对编程失去兴趣,但从那时起从未写过一行。所以他的参考不是“任何像样的编译器”,正如一些答案所提到的,而是“这个人在 20-30 年前使用的编译器”。
那时,由于“中央小型机”的 CPU 时间非常宝贵,程序员们花了相当多的时间尝试各种方法来使他们的代码更快、更高效。就像编写编译器的人一样。我猜他的公司当时提供的唯一编译器基于“经常遇到的可以优化的语句”进行了优化,并在遇到一段时间(1)时采取了一些捷径并评估了一切否则,包括一段时间(2)。有过这样的经历可以解释他的立场和他对此的信心。
让你被录用的最好方法可能是让高级经理得意忘形,在你顺利引导他进入下一个面试主题之前,用 2-3 分钟就“编程的美好时光”向你讲授 2-3 分钟。 (好的时机在这里很重要——太快,你会打断故事——太慢,你会被贴上注意力不足的标签)。一定要在采访结束时告诉他,你非常有兴趣了解更多关于这个话题的信息。
你应该问他是怎么得出这个结论的。在任何像样的编译器下,两者都编译为相同的 asm 指令。所以,他应该告诉你编译器也开始了。即便如此,您也必须非常了解编译器和平台,才能做出有理论根据的猜测。最后,在实践中这并不重要,因为还有其他外部因素,如内存碎片或系统负载,会比这个细节更能影响循环。
为了这个问题,我应该补充一下,我记得 C 委员会的 Doug Gwyn 写道,一些没有优化器通过的早期 C 编译器会在汇编中为 while(1)
生成一个测试(与 for(;;)
相比,它没有它)。
我会通过给出这个历史记录来回答面试官,然后说即使我对任何编译器这样做感到非常惊讶,编译器也可以:
如果没有优化器通过,编译器会为 while(1) 和 while(2) 生成一个测试
通过优化器传递,编译器被指示优化(无条件跳转)所有 while(1),因为它们被认为是惯用的。这将使 while(2) 进行测试,因此会在两者之间产生性能差异。
我当然会向面试官补充说,不考虑 while(1)
和 while(2)
相同的构造是低质量优化的标志,因为它们是等效的构造。
对此类问题的另一种看法是,看看您是否有勇气告诉您的经理他/她错了!以及你可以多么轻柔地传达它。
我的第一直觉是生成汇编输出,以向经理表明任何体面的编译器都应该处理它,如果它不这样做,您将为其提交下一个补丁 :)
看到这么多人深入研究这个问题,说明了为什么这很可能是一个测试,看看你想多快地对事物进行微优化。
我的回答是;没关系,我宁愿专注于我们正在解决的业务问题。毕竟,这就是我要付出的代价。
此外,我会选择 while(1) {}
,因为它更常见,其他队友不需要花时间去弄清楚为什么有人会选择高于 1 的数字。
现在去写一些代码。 ;-)
如果你担心优化,你应该使用
for (;;)
因为那没有测试。 (愤世嫉俗的模式)
在我看来,这是被伪装成技术问题的行为面试问题之一。有些公司会这样做——他们会问一个技术问题,对于任何称职的程序员来说,这个问题应该很容易回答,但是当面试者给出正确答案时,面试官会告诉他们他们错了。
公司想看看你在这种情况下会如何反应。你是否安静地坐在那里,不要因为自我怀疑或害怕惹恼面试官而强求你的答案是正确的?或者你愿意挑战一个你知道是错误的权威人士吗?他们想看看你是否愿意为自己的信念挺身而出,以及你是否能以委婉和尊重的方式做到这一点。
当这种胡说八道可能会有所作为时,我曾经对 C 和汇编代码进行编程。当它确实有所作为时,我们在 Assembly 中编写了它。
如果我被问到这个问题,我会重复 Donald Knuth 1974 年关于过早优化的著名名言,如果面试官没有笑并继续前进,我会走路。
可能面试官故意提了这么傻的问题,想让你说3点:
基本推理。两个循环都是无限的,很难谈论性能。关于优化级别的知识。他想听听你是否让编译器为你做任何优化,它会优化条件,特别是如果块不为空。了解微处理器架构。大多数架构都有一个特殊的 CPU 指令用于与 0 进行比较(虽然不一定更快)。
这里有一个问题:如果你真的编写一个程序并测量它的速度,两个循环的速度可能不同!对于一些合理的比较:
unsigned long i = 0;
while (1) { if (++i == 1000000000) break; }
unsigned long i = 0;
while (2) { if (++i == 1000000000) break; }
添加了一些打印时间的代码,一些随机效果(例如循环如何定位在一两个缓存行中)可能会产生影响。一个循环可能完全在一个高速缓存行内,或者在一个高速缓存行的开头,或者它可能跨越两个高速缓存行。结果,无论面试官声称最快的是什么,实际上都可能是最快的——巧合。
最坏的情况:优化编译器不知道循环做了什么,但知道执行第二个循环时产生的值与第一个循环产生的值相同。并为第一个循环生成完整代码,但不是为第二个循环。
他们都是平等的——同样的。
根据规范,任何不是 0 的东西都被认为是真的,所以即使没有任何优化,一个好的编译器也不会为 while(1) 或 while(2) 生成任何代码。编译器会为 != 0
生成一个简单的检查。
从人们花在测试、证明和回答这个非常直截了当的问题上的时间和精力来看,我说这两个问题都被问得很慢。
所以要花更多的时间在上面......
“while (2)”很荒谬,因为,
“while (1)”和“while (true)”在历史上被用于创建一个无限循环,该循环期望在循环内部的某个阶段根据肯定会发生的条件调用“break”。
“1”只是为了始终评估为真,因此,说“while (2)”与说“while (1 + 1 == 2)”也将评估为 true 一样愚蠢。
如果你想完全傻,只需使用: -
while (1 + 5 - 2 - (1 * 3) == 0.5 - 4 + ((9 * 2) / 4.0)) {
if (succeed())
break;
}
我想认为您的编码员犯了一个不影响代码运行的错字,但如果他故意使用“2”只是为了变得怪异,那么在他把奇怪的东西放在你的代码中之前解雇他!阅读和使用。
这取决于编译器。
如果它优化代码,或者如果它对特定指令集使用相同数量的指令将 1 和 2 评估为真,则执行速度将是相同的。
在实际情况下,它总是同样快,但是可以想象一个特定的编译器和一个特定的系统,当它们的评估方式不同时。
我的意思是:这实际上不是与语言(C)相关的问题。
由于希望回答这个问题的人想要最快的循环,我会回答两者都同样编译成相同的汇编代码,如其他答案中所述。不过,您可以使用“循环展开”向面试官建议; do {} while 循环而不是 while 循环。
小心:您需要确保循环至少总是运行一次。
循环内部应该有一个中断条件。
同样对于那种循环,我个人更喜欢使用 do {} while(42) 因为除了 0 之外的任何整数都可以完成这项工作。
显而易见的答案是:正如所发布的,两个片段都会运行一个同样繁忙的无限循环,这使得程序无限慢。
尽管将 C 关键字重新定义为宏在技术上会具有未定义的行为,但这是我能想到的使任一代码片段快速的唯一方法:您可以在 2 个片段上方添加这一行:
#define while(x) sleep(x);
它确实会使 while(1)
的速度是 while(2)
的两倍(或速度的一半)。
我能想到为什么 while(2)
会慢的唯一原因是:
该代码将循环优化为 cmp eax, 2 当减法发生时,您实际上是在减去 a。 00000000 - 00000010 cmp eax, 2 而不是 b。 00000000 - 00000001 cmp eax, 1
cmp
只设置标志,不设置结果。所以在最低有效位上,我们知道我们是否需要用 b 借用。而对于 a,您必须执行两次减法才能获得借位。
-O
级别产生相同的程序集。jmp
回到开头或完全删除循环。break
语句的可能性除外),但我还是测试了它,没有,即使循环内有代码对于while(1)
和while(2)
,跳转是绝对且无条件的。如果您真的担心,请随意测试其他人。