【问题标题】:while (1) Vs. for (;;) Is there a speed difference?而(1)对比。 for (;;) 有速度差异吗?
【发布时间】:2010-10-27 12:28:59
【问题描述】:

长版...

一位同事今天在看到我在 Perl 脚本中使用 while (1) 后断言 for (;;) 更快。我认为它们应该是相同的,希望解释器能够优化任何差异。我设置了一个脚本,它将运行 1,000,000,000 次循环迭代和相同数量的 while 循环并记录其间的时间。我找不到明显的区别。我的同事说,一位教授告诉他while (1) 正在比较1 == 1for (;;) 不是。我们用 100 倍的 C++ 迭代次数重复了相同的测试,差异可以忽略不计。然而,它是一个图形示例,说明编译代码与脚本语言相比要快多少。

短版...

如果您需要跳出无限循环,有什么理由更喜欢while (1) 而不是for (;;)

注意:如果从问题中不清楚。这纯粹是几个朋友之间有趣的学术讨论。我知道这不是一个所有程序员都应该为之苦恼的超级重要的概念。感谢所有出色的答案,我(我相信其他人)从这次讨论中学到了一些东西。

更新:上述同事在下面给出了回应。

在此引用以防被掩埋。

它来自一位 AMD 汇编程序员。他说 C 程序员 (人们)没有意识到他们的代码效率低下。他说 不过今天,gcc 编译器非常好,把他这样的人淘汰了 的业务。例如,他说,并告诉我while 1 vs for(;;)。我现在出于习惯使用它,但 gcc 尤其是口译员 这两天都将执行相同的操作(处理器跳转), 因为它们已经过优化。

【问题讨论】:

  • 我很好奇。为什么在 perl 脚本中需要无限循环?您显然不是在编写驱动程序或系统的东西... Infinite 很安静:-)
  • 哪个无限循环最快?大声笑...“我的新电脑速度非常快,不到一个小时就无限循环...”;-)
  • 是社会学教授告诉他的吗?在现代,您输入的代码并不是计算机最终看到的。
  • 我希望您测试它所花费的时间远远长于知道哪个更快(如果有的话)可能节省的时间。即使你在你的编程生涯中分期付款。
  • 为什么编译器会生成代码来执行它知道没有副作用并且编译器已经知道其结果的测试?这没有任何意义。

标签: c++ perl optimization performance


【解决方案1】:

在 perl 中,它们产生相同的操作码:

$ perl -MO=Concise -e 'for(;;) { print "foo\n" }'
a  <@> leave[1 ref] vKP/REFC ->(end)
1     <0> enter ->2
2     <;> nextstate(main 2 -e:1) v ->3
9     <2> leaveloop vK/2 ->a
3        <{> enterloop(next->8 last->9 redo->4) v ->4
-        <@> lineseq vK ->9
4           <;> nextstate(main 1 -e:1) v ->5
7           <@> print vK ->8
5              <0> pushmark s ->6
6              <$> const[PV "foo\n"] s ->7
8           <0> unstack v ->4
-e syntax OK

$ perl -MO=Concise -e 'while(1) { print "foo\n" }'
a  <@> leave[1 ref] vKP/REFC ->(end)
1     <0> enter ->2
2     <;> nextstate(main 2 -e:1) v ->3
9     <2> leaveloop vK/2 ->a
3        <{> enterloop(next->8 last->9 redo->4) v ->4
-        <@> lineseq vK ->9
4           <;> nextstate(main 1 -e:1) v ->5
7           <@> print vK ->8
5              <0> pushmark s ->6
6              <$> const[PV "foo\n"] s ->7
8           <0> unstack v ->4
-e syntax OK

在 GCC 中也是如此:

#include <stdio.h>

void t_while() {
    while(1)
        printf("foo\n");
}

void t_for() {
    for(;;)
        printf("foo\n");
}

    .file   "test.c"
    .section    .rodata
.LC0:
    .string "foo"
    .text
.globl t_while
    .type   t_while, @function
t_while:
.LFB2:
    pushq   %rbp
.LCFI0:
    movq    %rsp, %rbp
.LCFI1:
.L2:
    movl    $.LC0, %edi
    call    puts
    jmp .L2
.LFE2:
    .size   t_while, .-t_while
.globl t_for
    .type   t_for, @function
t_for:
.LFB3:
    pushq   %rbp
.LCFI2:
    movq    %rsp, %rbp
.LCFI3:
.L5:
    movl    $.LC0, %edi
    call    puts
    jmp .L5
.LFE3:
    .size   t_for, .-t_for
    .section    .eh_frame,"a",@progbits
.Lframe1:
    .long   .LECIE1-.LSCIE1
.LSCIE1:
    .long   0x0
    .byte   0x1
    .string "zR"
    .uleb128 0x1
    .sleb128 -8
    .byte   0x10
    .uleb128 0x1
    .byte   0x3
    .byte   0xc
    .uleb128 0x7
    .uleb128 0x8
    .byte   0x90
    .uleb128 0x1
    .align 8
.LECIE1:
.LSFDE1:
    .long   .LEFDE1-.LASFDE1
.LASFDE1:
    .long   .LASFDE1-.Lframe1
    .long   .LFB2
    .long   .LFE2-.LFB2
    .uleb128 0x0
    .byte   0x4
    .long   .LCFI0-.LFB2
    .byte   0xe
    .uleb128 0x10
    .byte   0x86
    .uleb128 0x2
    .byte   0x4
    .long   .LCFI1-.LCFI0
    .byte   0xd
    .uleb128 0x6
    .align 8
.LEFDE1:
.LSFDE3:
    .long   .LEFDE3-.LASFDE3
.LASFDE3:
    .long   .LASFDE3-.Lframe1
    .long   .LFB3
    .long   .LFE3-.LFB3
    .uleb128 0x0
    .byte   0x4
    .long   .LCFI2-.LFB3
    .byte   0xe
    .uleb128 0x10
    .byte   0x86
    .uleb128 0x2
    .byte   0x4
    .long   .LCFI3-.LCFI2
    .byte   0xd
    .uleb128 0x6
    .align 8
.LEFDE3:
    .ident  "GCC: (Ubuntu 4.3.3-5ubuntu4) 4.3.3"
    .section    .note.GNU-stack,"",@progbits

所以我猜答案是,它们在许多编译器中是相同的。当然,对于其他一些编译器,情况可能不一定如此,但循环内部的代码很可能会比循环本身贵几千倍,所以谁在乎呢?

【讨论】:

  • 尝试使用 B::Deparse,解析无限 for 循环会返回一个 while 循环:P
  • “在 perl 中,它们产生相同的操作码”...是的,但哪个更快? :-)
  • 我喜欢 gcc 用 puts() 代替 printf(),因为只有一个参数,因此无需格式化 - 更快更安全! (gcc 还会根据变量参数列表检查格式化标签。)
  • @the Tin Man:它们是等价的,因为计算机执行相同的操作:P
  • @snap,这不是“完全”不正确,它只是关注运行时成本。我无法想象什么样的情况会导致无限循环的解析时间成为程序运行速度的关键决定因素
【解决方案2】:

没有太多理由偏爱其中之一。我确实认为while(1) 尤其是while(true)for(;;) 更具可读性,但这只是我的偏好。

【讨论】:

  • #define EVER ;; for(EVER) 我一直觉得那种很有趣。
  • #define ever (;;) forever 怎么样;
  • 从表面上看,两者都更具可读性,但我尽量不为我的维护程序员(通常是我)定义新的关键字来让他摸不着头脑。
  • @Martin 不起作用,因为 #define 不会在令牌内替换,而 forever 是它自己的令牌。
  • “我尽量不为我的维护定义新的关键字”——如果有更多的人采取这种态度,我就不会每次转身都抓住所有这些愚蠢而神奇的花招周围!
【解决方案3】:

使用 GCC,它们似乎都编译为相同的汇编语言:

L2:
        jmp     L2

【讨论】:

  • 将 GCC 与 -S 选项一起使用(组装,不要链接)
【解决方案4】:

根据标准没有区别。 6.5.3/1 有:

for 语句

for ( for-init-statement ; conditionopt ; expressionopt ) statement

等价于

{
  for-init-statement
  while ( condition ) {
    statement
    expression ;
  }
}

而 6.5.3/2 有:

条件和表达式中的一个或两个都可以省略。缺少条件使得隐含的 while 子句等同于 while(true)。

所以根据C++标准代码:

for (;;);

完全一样:

{
  while (true) {
    ;
    ;
  }
}

【讨论】:

  • 这根本与生成的代码或性能无关。该标准仅定义功能。当然,性能是一样的。
  • 我不认为性能上的差异违反了假设规则。如果是这样,那么编译器将不允许在 as-if 规则下加速您的代码,例如通过重新排序独立语句。编译器实际上就是这样做的。但我的标准副本在楼上。
【解决方案5】:

for(;;) 如果您想朝那个方向优化,可以少输入一个字符。

【讨论】:

  • 打高尔夫球的好消息。否则选择语法的理由很糟糕。
  • @AdamBellare Terseness 通常会提高可读性,超过一定的技能门槛。
【解决方案6】:

用于发出警告的 Visual C++ 编译器

while (1) 

(常量表达式)但不适用于

for (;;)

出于这个原因,我继续首选for (;;),但我不知道编译器这些天是否仍然这样做。

【讨论】:

  • 警告可能是因为您使用了 while(1) 而不是 while(true)
  • true 是一个常数。 while (true) 是一个常量表达式。对于任何感兴趣的人,此处记录了警告 C4127:msdn.microsoft.com/en-us/library/6t66728h(VS.80).aspx
  • 是的,警告仍然存在于 1 和 true 中。这就是为什么我总是使用 for (;;)
  • @ElvissStrazdins Visual Studio 的文档现在说 C4127 不会为 while(1)while(true) 发出警告。
  • 文档现在声明:“由于它们常见的惯用用法,从 Visual Studio 2015 更新 3 开始,诸如 1 或 true 之类的普通常量不会触发警告,除非它们是操作的结果在表达式中。”
【解决方案7】:

使用这种旧编译器 for(;;) 的 Turbo C 生成的代码比 while(1) 更快。

今天的 gcc,Visual C(我认为几乎所有)编译器都优化得很好,很少使用 4.7 MHz 的 CPU。

在那些日子里,for( i=10; i; i-- )for( i=1; i &lt;=10; i++ ) 快​​,因为比较 i 为 0,导致 CPU-Zero-Flag 条件跳转。并且用最后的减量操作( i-- )修改了零标志,不需要额外的cmp操作。

    call    __printf_chk
    decl    %ebx          %ebx=iterator i 
    jnz     .L2
    movl    -4(%ebp), %ebx
    leave

这里有 for(i=1; i&lt;=10; i++) 和额外的 cmpl:

    call    __printf_chk
    incl    %ebx
    cmpl    $11, %ebx
    jne     .L2
    movl    -4(%ebp), %ebx
    leave

【讨论】:

    【解决方案8】:

    对于所有争论你不应该使用无限循环的人,并建议使用开放 goto 之类的愚蠢的东西(真的,哎哟)

    while (1) {
         last if( condition1 );
         code();
         more_code(); 
         last if( condition2 ); 
         even_more_code(); 
    }
    

    实际上无法以任何其他方式有效地表示。并非没有创建退出变量并执行黑魔法以使其保持同步。

    如果您更喜欢 goto-esque 语法,请使用一些限制范围的理智的东西。

    flow: { 
    
       if ( condition ){ 
          redo flow;
       }
       if ( othercondition ){ 
           redo flow;
       }
       if ( earlyexit ){ 
           last flow;
       }
       something(); # doesn't execute when earlyexit is true 
    }
    

    最终速度并不那么重要

    担心不同的循环结构在速度方面的有效性是浪费时间。通过和通过过早的优化。我想不出我见过的任何情况,其中分析代码在我选择循环构造时发现了瓶颈。

    通常是循环的方式和循环的what

    您应该“优化”可读性和简洁性,并编写最能向找到您的代码的下一个可怜的傻瓜解释问题的内容。

    如果你使用有人提到的“goto LABEL”技巧,而我必须使用你的代码,请准备好睁着眼睛睡觉,尤其是如果你不止一次这样做,因为那种东西会创建 可怕的意大利面条代码。

    仅仅因为您可以创建意大利面条式代码并不意味着您应该

    【讨论】:

      【解决方案9】:

      来自 Stroustrup,TC++PL(第 3 版),§6.1.1:

      奇怪的符号for (;;) 是指定无限循环的标准方法;你可以发音为“永远”。 [...] while (true) 是一个替代方案。

      我更喜欢for (;;)

      【讨论】:

        【解决方案10】:

        如果编译器不做任何优化,for(;;) 总是比while(true) 快。这是因为 while 语句每次都会评估条件,但 for 语句是无条件跳转。但是如果编译器优化了控制流,它可能会生成一些操作码。您可以非常轻松地阅读反汇编代码。

        附:你可以这样写一个无限循环:

        #define EVER ;;
          //...
          for (EVER) {
            //...
          }
        

        【讨论】:

        • 在当今时代,永远不应该用 EVS(青少年说话)代替!说真的,虽然我只是简单地使用 for(;;){}。很久以前我在网上读到了两者之间的差异(当我年轻的时候,实际上并不知道它们是一样的),并且坚持我所读到的。
        【解决方案11】:

        我听说过一次。

        它来自一位 AMD 汇编程序员。他说 C 程序员(人们)没有意识到他们的代码效率低下。不过他今天说,gcc 编译器非常好,让像他这样的人破产。例如,他说,并告诉我while 1 vs for(;;)。我现在出于习惯使用它,但 gcc 尤其是解释器在这两天都会执行相同的操作(处理器跳转),因为它们已经过优化。

        【讨论】:

          【解决方案12】:

          在编译语言的优化构建中,两者之间应该没有明显的区别。两者都不应该最终在运行时执行任何比较,它们只会执行循环代码,直到您手动退出循环(例如使用break)。

          【讨论】:

            【解决方案13】:

            我很惊讶没有人在 perl 中正确测试 for (;;)while (1)

            因为 perl 是解释型语言,运行 perl 脚本的时间不仅包括执行阶段(在这种情况下是相同的),还包括执行前的解释阶段。在进行速度比较时,必须考虑这两个阶段。

            幸运的是 perl 有一个方便的Benchmark module,我们可以使用它来实现如下基准:

            #!/usr/bin/perl -w
            
            use Benchmark qw( cmpthese );
            
            sub t_for   { eval 'die; for (;;) { }'; }
            sub t_for2  { eval 'die; for (;;)  { }'; }
            sub t_while { eval 'die; while (1) { }'; }
            
            cmpthese(-60, { for => \&t_for, for2 => \&t_for2, while => \&t_while });
            

            请注意,我正在测试无限 for 循环的两种不同版本:一种比 while 循环短,另一种具有额外的空间以使其与 while 循环的长度相同。

            在带有 perl 5.10.1 的 Ubuntu 11.04 x86_64 上,我得到以下结果:

            为 for2 评分,而 对于 100588/s -- -0% -2% for2 100937/s 0% -- -1% 而 102147/s 2% 1% --

            while 循环显然是这个平台上的赢家。

            在带有 perl 5.14.1 的 FreeBSD 8.2 x86_64 上:

            为 for2 评分,而 对于 53453/s -- -0% -2% for2 53552/s 0% -- -2% 而 54564/s 2% 2% --

            虽然循环在这里也是赢家。

            在带有 perl 5.14.1 的 FreeBSD 8.2 i386 上:

            为 for2 评分 而 24311/s -- -1% -1% 对于 24481/s 1% -- -1% for2 24637/s 1% 1% --

            令人惊讶的是,带有额外空格的 for 循环是这里最快的选择!

            我的结论是,如果程序员正在优化速度,那么应该在 x86_64 平台上使用 while 循环。显然,在优化空间时应该使用 for 循环。不幸的是,我的结果对于其他平台尚无定论。

            【讨论】:

            • 结论大错特错。 Benchmark 有其局限性,如果结果在 7% 以内,则不能用于区分快慢。此外,您还没有测试forwhile 循环之间的区别,因为每个子在到达循环之前都会die。并且从什么时候开始对 Perl 解释器来说,空格的数量很重要?抱歉,分析有很大缺陷。
            • @Zaid,感谢您的 cmets!您介意发布自己的答案,以便每个人都可以从中学习吗? :) die 存在于我的代码中,因为我的意图是仅测试编译时间差异。正如其他人已经指出的那样,生成的字节码是相同的,因此没有必要对其进行测试。令人惊讶的是,在我的测试环境中,在这种情况下,空白的数量似乎产生了很小的差异。这可能与字符最终如何在内存中对齐或类似的东西有关......
            • 我不需要发布答案,因为我要说的已经被 bdonlan 提到了。即使您在比较编译时间,Benchmark 的数字也是不确定的。根本不要相信那 1% 的差异!
            • 只有 60 次迭代?运行 5 分钟左右的测试,以获得更准确的相对时间。
            • -60 运行测试 60 秒。
            【解决方案14】:

            刚刚看到这个帖子(虽然晚了好几年)。

            我想我找到了“for(;;)”比“while(1)”更好的真正原因。

            根据《2018年barr编码标准》

            Kernighan & Ritchie long ago recommended for (;;) , which has the additional benefit
            of insuring against the visually-confusing defect of a while (l); referencing a variable ‘l’.
            

            基本上,这不是速度问题,而是可读性问题。根据代码的字体/打印,一段时间内的数字 one(1) 可能看起来像一个小写字母 l。

            即 1 对 l。 (在某些字体中,它们看起来相同)。

            所以 while(1) 可能看起来像一些依赖于变量字母 L 的 while 循环。

            while(true) 也可以工作,但在一些较旧的 C 和嵌入式 C 案例中,除非包含 stdbool.h,否则尚未定义真/假。

            【讨论】:

            • 我会说你代码中的问题是你有一个名为l的变量,而不是1l看起来相似。
            • 同意,我知道 Barr 编码标准还在其他地方规定,即使在 for 循环中,变量也必须至少为 3 个字符。即 for 循环中没有 i++ 等。我倾向于认为这可能有点多。在打字时,我还注意到不仅仅是字母 L 看起来像 1。通常用作变量的字母 i 也可能导致问题。
            【解决方案15】:

            理论上,一个完全天真的编译器可以将文字“1”存储在二进制文件中(浪费空间)并检查每次迭代是否 1 == 0(浪费时间和更多空间)。

            然而,实际上,即使“没有”优化,编译器仍会将两者缩减为相同。它们也可能发出警告,因为它可能指示逻辑错误。例如,while 的参数可以在其他地方定义,而您没有意识到它是常量。

            【讨论】:

              【解决方案16】:

              我很惊讶没有人提供更直接的形式,对应于所需的程序集:

              forever:
                   do stuff;
                   goto forever;
              

              【讨论】:

              • 在说 c 中的结果与 while 1 或 for (;;) 的机器代码不同吗?
              • 该方法的另一个缺点:它违反了封装,因为它没有将循环封闭在一个块中——因此在循环中声明的任何变量都可以在循环外使用。 (当然可以{ forever: do stuff; goto forever; }
              【解决方案17】:

              while(1)for(;;) 的一个成语,大多数编译器都能识别。

              我很高兴看到 perl 也能识别 until(0)

              【讨论】:

              • 在什么情况下直到(0) 有用?
              • until() 是 while() 的反义词,就像 unless() 是 if() 的反义词一样。正如该线程中其他地方所建议的那样,人们可能会这样写:do { something... } while (!condition) 另一种选择可能是 until (condition) { something }
              【解决方案18】:

              总结 for (;;)while (1) 的争论很明显,前者在旧的非优化编译器时代更快,这就是为什么你倾向于在旧代码库(如 Lions Unix 源代码)中看到它评论,但是在坏蛋优化编译器的时代,这些收益被优化掉了,再加上后者比前者更容易理解,我相信它会更可取。

              【讨论】:

              • 推测编译器不知道如何为while(true) 创建高效汇编的人没有提供任何证据。
              【解决方案19】:

              我认为两者在性能方面是相同的。但我更喜欢 while(1) 以提高可读性,但我质疑为什么需要无限循环。

              【讨论】:

              • 如果你想不出一个无限循环的理由,我建议你开始更频繁地编写一些代码。
              【解决方案20】:

              它们是一样的。还有很多更重要的问题需要思考。


              我在上面暗示但没有明确提出的观点是,一个体面的编译器将为两种循环形式生成完全相同的代码。更重要的一点是循环结构是任何算法运行时间的一小部分,您必须首先确保您已经优化了算法以及与之相关的所有其他内容。优化你的循环结构绝对应该在你的优先级列表的底部。

              【讨论】:

              • 没有链接或解释。没有帮助,主观且有点居高临下。
              • 好吧,没有证据,但他是对的。他们都调用操作码为假时跳转。 (这将使其与 goto 相同,但没有人喜欢 goto)
              • 我不知道只有重要的问题要问,我的错误是我的第一个问题。
              • 是的,我承认这是居高临下的。但说真的,即使没有任何证据,很明显他们将在同一个球场上快速发展。如果问题是关于风格的,就会有一些争论。我试图指出,在需要担心的事情列表中,这确实应该位于列表的底部。
              • 我并不是想成为一个混蛋。我试图说明一点。当我发布它时,我正在尝试一种黑色幽默,很明显我失败了;对此我深表歉意。
              猜你喜欢
              • 2010-09-20
              • 2010-12-17
              • 2023-01-25
              • 2017-05-22
              • 1970-01-01
              • 1970-01-01
              • 2020-07-24
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多