关于C#:哪个更快:while(1)或while(2)?

Which is faster: while(1) or while(2)?

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

哪个更快?

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

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

我说这两个都有相同的执行速度,因为while中的表达式最终应该对truefalse进行评估。在这种情况下,都对true进行评估,并且在while条件中没有额外的条件指令。因此,两种方法的执行速度相同,我更喜欢while(1)。

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

这是真的吗?

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


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

使用gcc,我编译了以下两个程序,以在不同的优化级别进行组装:

1
2
3
4
int main(void) {
    while(1) {}
    return 0;
}
1
2
3
4
int main(void) {
    while(2) {}
    return 0;
}

即使没有优化(-O0),生成的程序集对于两个程序都是相同的。因此,两个回路之间没有速度差。

以下是生成的程序集(使用带优化标志的gcc main.c -S -masm=intel):

使用-O0时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    .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时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    .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时(输出相同):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    .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"

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

1
2
3
4
 .L2:
    jmp .L2
    .seh_endproc
    .ident "GCC: (tdm64-2) 4.8.1"

重要的是:

1
2
.L2:
    jmp .L2

我不能很好地阅读汇编,但这显然是一个无条件循环。jmp指令无条件地将程序重置回.L2标签,而不必将值与真值进行比较,当然,在程序以某种方式结束之前,它会立即再次这样做。这直接对应于C/C++代码:

1
2
L2:
    goto L2;

编辑:

有趣的是,即使没有优化,以下循环在程序集中都产生了完全相同的输出(无条件的jmp)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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) {}

甚至令我惊讶的是:

1
2
3
4
5
#include<math.h>

while(sqrt(7)) {}

while(hypot(3,4)) {}

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

1
2
3
4
5
int x(void) {
    return 1;
}

while(x()) {}

< BR>

1
2
3
4
5
6
7
#include<math.h>

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

while(x()) {}

-O0中,这两个示例实际上称为x并对每个迭代进行比较。

第一个示例(返回1):

1
2
3
4
5
6
7
8
9
10
.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)):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.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回到前面的标签)。

DR

在gcc下,不同的循环被编译成相同的程序集。编译器计算常量值,不需要执行任何实际比较。

故事的寓意是:

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


是的,对于一个人来说,while(1)while(2)快得多!如果我在一个不熟悉的代码库中看到while(1),我马上就知道作者的意图,我的眼球可以继续往下一行。

如果我看到while(2),我可能会停下脚步,试图弄明白作者为什么不写while(1)。作者的手指在键盘上滑了吗?这个代码库的维护者是否使用while(n)作为一个模糊的注释机制,使循环看起来不同?在一些损坏的静态分析工具中,这是一个错误警告的粗略解决方法吗?或者这是我正在读取生成代码的线索?它是一个错误的发现和替换所有的,还是一个错误的合并,还是宇宙射线?也许这行代码应该做一些显著不同的事情。也许应该读到while(w)while(x2)。我最好在文件的历史记录中找到作者,然后给他们发一封"wtf"电子邮件…现在我已经打破了我的精神环境。埃多克斯一号〔6〕可能会占用我几分钟的时间,而此时,埃多克斯一号〔5〕可能只花了不到一秒钟的时间!

我有点夸张,但只是有点夸张。代码可读性非常重要。这在面试中值得一提!


显示特定编译器为具有特定选项集的特定目标生成的代码的现有答案并不能完全回答该问题,除非在特定上下文中提出该问题("使用带有默认选项的gcc 4.7.2更快吗?"例如。

就语言定义而言,在抽象机中,while (1)计算整数常数1while (2)计算整数常数2;在这两种情况下,结果都比较等于零。语言标准完全没有说明这两个构造的相对性能。

我可以想象,一个非常幼稚的编译器可能会为这两种形式生成不同的机器代码,至少在编译时不会请求优化。

另一方面,当某些常量表达式出现在需要常量表达式的上下文中时,C编译器必须在编译时对它们进行计算。例如,这:

1
2
3
4
5
int n = 4;
switch (n) {
    case 2+2: break;
    case 4:   break;
}

需要诊断;懒惰的编译器不能选择将2+2的计算推迟到执行时间。因为编译器必须能够在编译时计算常量表达式,所以即使不需要它,也没有充分的理由不利用这个功能。

C标准(N1570 6.8.5P4)规定

An iteration statement causes a statement called the loop body to be
executed repeatedly until the controlling expression compares equal to
0.

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

一个非常幼稚的编译器可以为这两个构造生成不同的代码。例如,首先它可以生成一个无条件的无限循环(将1视为特殊情况),其次它可以生成一个与2 != 0等效的显式运行时比较。但我从来没有遇到过一个真正的C编译器,它会以这种方式运行,我严重怀疑这样的编译器是否存在。

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

如果编译器为这两个构造生成不同的代码,首先检查不同的代码序列是否具有不同的性能。如果有,请使用优化选项(如果可用)再次尝试编译。如果它们仍然不同,请向编译器供应商提交一份错误报告。从未能符合C标准的意义上来说,这不一定是一个bug,但几乎可以肯定这是一个应该纠正的问题。

底线:while (1)while(2)几乎肯定具有相同的性能。它们的语义完全相同,任何编译器都没有理由不生成相同的代码。

虽然编译器为while(1)生成的代码比为while(2)生成的代码更快是完全合法的,但编译器为while(1)生成的代码比同一程序中出现的while(1)生成的代码更快也是同样合法的。

(你问的另一个问题是:你如何处理一个坚持错误技术观点的面试官。对于工作场所站点来说,这可能是一个很好的问题)。


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

enter image description here

面试官本人没有通过这次面试已经够糟糕的了,如果这家公司的其他程序员"通过"了这个测试呢?

不,评估声明1 == 02 == 0应该同样迅速。我们可以想象糟糕的编译器实现,其中一个可能比另一个更快。但没有充分的理由解释为什么一个要比另一个快。

即使在某些模糊的情况下声明是正确的,程序员也不应该基于模糊(在本例中是令人毛骨悚然)的知识来进行评估。别担心这次面试,这里最好的办法就是走开。

免责声明:这不是原创的迪尔伯特卡通。这只是一个混搭。


你的解释是正确的。这似乎是一个除了技术知识之外还考验你自信的问题。

顺便问一下,如果你回答

Both pieces of code are equally fast, because both take infinite time to complete

面试官会说

But while (1) can do more iterations per second; can you explain why? (this is nonsense; testing your confidence again)

所以像你那样回答,你节省了一些时间,否则你会浪费在讨论这个坏问题上。

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

1
2
3
4
5
6
7
yyy:
    xor eax, eax
    cmp eax, 1     (or 2, depending on your code)
    je xxx
    jmp yyy
xxx:
    ...

启用优化后:

1
2
xxx:
    jmp xxx

所以生成的代码是完全相同的,至少在优化编译器中是一样的。


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

1
2
1 = 00000001
2 = 00000010

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

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


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

他们把你逼入冲突。如果这是真的,他们是聪明的,问题是好的。对于某些行业,如银行业,将问题发布到堆栈溢出可能是拒绝的原因。

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


我想线索应该在"高级经理问"中找到。当他成为经理后,这个人显然停止了编程,然后他/她花了几年时间才成为高级经理。从没有对编程失去兴趣,但从那以后就再也没有写过一行。因此,正如一些答案所提到的,他的参考不是"任何一个合适的编译器",而是"这个人20-30年前工作过的编译器"。

当时,程序员花了相当大的一部分时间尝试各种方法,使他们的代码更快、更高效,因为"中央微型计算机"的CPU时间是如此宝贵。就像编写编译器的人一样。我猜他公司当时提供的唯一一个编译器是基于"经常遇到的可以优化的语句"进行优化的,并且在遇到一段时间(1)和评估所有其他内容(包括一段时间(2))时采取了一些捷径。有了这样的经验可以解释他的立场和对它的信心。

最好的聘用方法可能是让高级经理能够在你顺利引导他进入下一个面试主题之前,对你进行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更高的号码。

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


在我看来,这是一个伪装成技术问题的行为面试问题。有些公司会这样做——他们会问一个技术问题,这对于任何有能力的程序员来说都是相当容易回答的,但是当被采访者给出正确的答案时,采访者会告诉他们他们是错的。

公司想看看你在这种情况下会有什么反应。你是否因为自我怀疑或害怕打乱面试官而静静地坐在那里,不去强迫自己的回答是正确的?或者你愿意挑战一个你知道错误的权威人士?他们想知道你是否愿意为自己的信念辩护,以及你是否能以一种得体和恭敬的方式做到这一点。


如果你担心优化,你应该使用

1
for (;;)

因为没有测试。(犬儒模式)


我曾经在这种胡说八道的情况下对C和汇编代码进行编程。当它确实起了作用时,我们把它写在汇编中。

如果我被问到这个问题,我会重复唐纳德·克努斯1974年著名的关于过早优化的引述,如果采访者不笑,继续前进,我会走。


也许面试官故意提出这样一个愚蠢的问题,想让你说3点:

  • 基本推理。两个循环都是无限的,很难谈论性能。
  • 了解优化水平。他想听听你的意见,如果你让编译器为你做任何优化,它将优化条件,特别是如果块不是空的。
  • 了解微处理器架构。大多数架构都有一个特殊的CPU指令来与0进行比较(但不一定更快)。

  • 从人们花在测试、证明和回答这个非常直截了当的问题上的时间和精力来看,我认为这两个问题都是通过问这个问题而变得非常缓慢的。

    所以花更多的时间在上面…

    "while(2)"是荒谬的,因为,

    "while(1)"和"while(true)"在历史上用于生成无限循环,该循环期望在循环内部的某个阶段根据一定会发生的条件调用"break"。

    "1"只是简单地存在,总是评估为真,因此,说"while(2)"和说"while(1+1==2)"一样愚蠢,也将评估为真。

    如果你想完全愚蠢的话,就用-

    1
    2
    3
    4
    while (1 + 5 - 2 - (1 * 3) == 0.5 - 4 + ((9 * 2) / 4)) {
        if (succeed())
            break;
    }

    我想你的编码员做了一个打字错误,但没有影响代码的运行,但如果他故意使用"2"只是为了让人觉得奇怪,那么在他把奇怪的sh放出来之前就解雇他!t通过你的代码使阅读和工作变得困难。


    他们都是平等的——是一样的。

    根据规范,任何非0的东西都被认为是正确的,因此即使没有任何优化,一个好的编译器也不会生成任何代码。暂时(1)或暂时(2)。编译器将为!= 0生成一个简单的检查。


    这里有一个问题:如果你真的写了一个程序并测量它的速度,两个循环的速度可能会不同!为了进行一些合理的比较:

    1
    2
    3
    4
    5
    unsigned long i = 0;
    while (1) { if (++i == 1000000000) break; }

    unsigned long i = 0;
    while (2) { if (++i == 1000000000) break; }

    添加了一些打印时间的代码后,一些随机效应(比如循环如何定位在一行或两行缓存中)可能会产生影响。一个循环可能完全在一条缓存线内,或者在一条缓存线的开头,或者跨越两条缓存线。结果,不管面试官说的是什么,最快的都可能是最快的——纯属巧合。

    最坏的情况:一个优化的编译器不知道循环是做什么的,但是发现在执行第二个循环时产生的值与第一个循环产生的值相同。并为第一个循环生成完整代码,但不为第二个循环生成完整代码。


    这取决于编译器。

    如果对代码进行优化,或者对特定指令集使用相同数量的指令将1和2计算为真,则执行速度将相同。

    在实际情况下,它的速度总是一样快,但是当计算结果不同时,可以想象一个特定的编译器和一个特定的系统。

    我的意思是:这不是一个与语言相关的问题。


    显而易见的答案是:正如贴出的,两个片段将运行一个同样繁忙的无限循环,这使得程序无限慢。

    虽然将C关键字重新定义为宏在技术上具有未定义的行为,但这是我能想到的唯一一种快速实现代码片段的方法:您可以将此行添加到2个片段之上:

    1
    #define while(x) sleep(x);

    它确实会使while(1)的速度是while(2)的两倍(或一半)。


    因为想要回答这个问题的人希望得到最快的循环,所以我会回答,正如其他答案中所述,这两个问题都被同样地编译成相同的汇编代码。不过,你可以向面试官建议使用"循环展开"的do while循环,而不是while循环。

    小心:您需要确保循环至少总是运行一次。

    循环内部应该有一个中断条件。

    另外,对于这种循环,我个人更喜欢使用do while(42),因为除0之外的任何整数都可以完成该任务。


    我能想到while(2)会变慢的唯一原因是:

  • 代码将循环优化为

    cmp eax, 2

  • 当减法发生时,基本上就是减法。

    A.江户十一〔17〕号

    而不是

    b.江户十一〔18〕号

  • cmp只设置标志,不设置结果。因此,对于最低有效位,我们知道是否需要借用b,而对于a,在借用之前必须执行两次减法。