【问题标题】:C/C++: is GOTO faster than WHILE and FOR?C/C++:GOTO 比 WHILE 和 FOR 快吗?
【发布时间】:2011-07-19 00:35:46
【问题描述】:

我知道,每个人都讨厌 GOTO,没有人推荐它。但这不是重点。我只想知道,哪个代码最快:

  1. goto 循环

    int i=3;
    loop:
    printf("something");
    if(--i) goto loop;
    
  2. while 循环

    int i=3;
    while(i--) {
        printf("something");
    }
    
  3. for 循环

    for(int i=3; i; i--) {
        printf("something");
    }
    

【问题讨论】:

  • 你已经写好了所有的代码......为什么不直接分析一下呢?
  • @Arturr 没有一个通用的答案。由于循环在功能上是相同的,因此您的问题正是关于编译器将如何处理它们。正如 Gabe 在下面所说,您会期望一个合理的编译器为所有三个循环提供相同的汇编代码。不过,这不是必需的,您可以编写一个编译器,无缘无故地让其中一个循环变慢。
  • 很可能这些都将编译为同一个程序集,执行增量cmpjmp。或者(通过给定的示例)在-O0 之后的任何级别,最终只会直接调用 printf() 3 次。
  • 对于这个严重误导的问题,我很想 -1,但这不是你的错。但现在以你的态度,-1。 没关系。选择正确的,让编译器完成它的工作,完成这一天。如果这是您的分析器报告的问题,请查看程序集,看看您是否可以解决它,不要试图做出荒谬的概括性概括。最后,goto 还不错,“因为某个聪明人这么说”,他说因为它很糟糕。你对反对它的建议轻率是没有根据的;有比goto 句号更好的方式来表达您的意图。
  • 在循环内用printf 提出这样的问题是荒谬的。您的 printf 将比您的循环控制花费更多的时间。在控件本身的处理变得相关之前,您确实必须在循环内有一些非常优化的代码(没有缓存未命中之类的东西)。

标签: c performance loops goto


【解决方案1】:

一般来说,forwhile 循环被编译为与goto 相同的东西,所以它通常不会有什么不同。如果您有疑问,可以随意尝试所有三个,看看哪个需要更长的时间。即使循环十亿次,您也可能无法衡量差异。

如果您查看this answer,您会发现编译器可以为forwhilegoto 生成完全相同的代码(仅在这种情况下没有条件)。

【讨论】:

  • 在某些情况下,引用答案中的“无条件”可能对断言至关重要,因为否则goto 版本可能会被转换为条件测试,然后是无条件分支而不是直接条件分支。
【解决方案2】:

我唯一一次在 W. Richard Stevens 的文章或书中看到关于 goto 的论点。他的观点是,在代码的非常时间关键部分(我相信他的示例是网络堆栈)中,可以使用 goto 重做具有相关错误处理代码的嵌套 if/else 块方式产生了宝贵的变化。

就我个人而言,我不是一个优秀的程序员,无法与 Stevens 的工作争论,所以我不会尝试。 goto 可以对与性能相关的问题很有用,但是 when 的限制是相当严格的。

【讨论】:

  • OP 询问的是 goto 与循环结构相比,而不是 if 结构。
  • 是的......你的答案是正确的。我挖出了(授予,从记忆中)我见过的关于 goto 与其他构造的唯一相关讨论。我的最后一句话是“答案”的关键。我不希望它成为公认的答案,但我相信它与一般讨论相关。
  • 当你想优化一些代码时,这是一个非常好的做法,我在一些极端情况下使用它,当速度更重要时。但无论如何,如果可以的话,我会投票支持你的回答。
【解决方案3】:

编写短程序,然后这样做:

gcc -S -O2 p1.c 
gcc -S -O2 p2.c 
gcc -S -O2 p3.c 

分析输出,看看是否有任何差异。一定要引入某种程度的不可预测性,这样编译器就不会将程序优化为一无所有。

编译器在优化这些琐碎的问题方面做得很好。我建议不要担心它,而是专注于让你作为程序员更有效率的东西。

速度和效率是一件值得担心的事情,但 99% 的时间都涉及使用正确的数据结构和算法……不用担心 for 是否比 while 或 @ 快987654325@等

【讨论】:

  • -O3 会给你全面优化
  • 对于那些尝试这个的人,请参阅我的回答here,了解有关如何为 g++ 和 clang++ 添加 cmets 的更多信息,以便您知道要查看程序集的哪个部分...跨度>
  • @Andrew 那和-flto
【解决方案4】:

只要您生成与正常循环相同的控制流,几乎任何体面的编译器都可以并且将生成相同的代码,无论您使用forwhile 等。

您可以从使用goto 中获得一些好处,但通常只有在您生成的控制流是普通循环根本无法(至少干净地)无法实现的情况下。一个典型的例子是跳到循环的中间来获得一个半循环结构,大多数语言的正常循环语句(包括 C 语言)都不能干净地提供。

【讨论】:

    【解决方案5】:

    所有循环和 goto 之间不应该有任何显着差异。除了这个想法,那个编译器更可能根本不会尝试优化 GOTO 的东西。

    尝试在循环中优化编译器生成的东西并没有多大意义。更有意义的是优化循环内的代码,或者减少迭代次数等等。

    【讨论】:

    • +1 表示优化循环内部更重要。
    【解决方案6】:

    它可能是编译器、优化器和架构特定的。

    例如,代码if(--i) goto loop; 是一个条件测试,后跟一个无条件分支。编译器可能会简单地生成相应的代码,或者它可能足够智能(尽管至少没有那么多智能的编译器可能不值得)生成单个条件分支指令。另一方面,while(i--) 已经是源代码级别的条件分支,因此无论编译器实现或优化器的复杂程度如何,都可能更容易转换为机器级别的条件分支。

    最后,差异可能很小,并且仅在需要大量迭代时才相关,您应该回答这个问题的方式是为特定目标和编译器(和编译器设置)构建代码感兴趣,然后检查生成的机器级代码或直接测量执行时间。

    在您的示例中,循环中的 printf() 在任何情况下都将支配任何时间;循环中更简单的东西会使观察差异更容易。我建议一个空循环,然后声明 i volatile 以防止循环被优化为空。

    【讨论】:

      【解决方案7】:

      在一些利基垂直领域,goto 仍然被一些非常聪明的人普遍用作标准做法,并且在这些设置中对 goto 没有偏见。我曾经在一家专注于模拟的公司工作,那里所有的本地 fortran 代码都有大量的 goto,团队非常聪明,软件运行几乎完美无缺。

      因此,我们可以将 goto 的优点放在一边,如果问题仅仅是比较循环,那么我们可以通过分析和/或比较汇编代码来做到这一点。尽管如此,问题包括诸如 printf 之类的语句。这样做时你不能真正讨论循环控制逻辑优化。此外,正如其他人所指出的,给定的循环都将生成非常相似的机器代码。

      除了通常将小循环扩展为无循环之外,在流水线处理器架构中,所有条件分支都被视为“已采用”(真),直到解码阶段。因此,根据上述 Harper 的观点,goto 在简单的循环控制中具有任何优势是不现实的(就像 for 或 while 彼此之间没有优势一样)。当将 goto 检查的附加条件添加到每个嵌套循环或嵌套 if 不是最佳条件时,GOTO 通常在多个嵌套循环或多个嵌套 if 中有意义。

      在简单循环中优化搜索类型的操作时,使用哨兵有时比其他任何方法都更有效。本质上,通过在数组末尾添加一个虚拟值,您可以避免将两个条件(数组末尾和找到的值)检查为只是一个条件(找到值),并且可以在内部节省 cmp 操作。我不知道编译器是否会自动这样做。

      【讨论】:

        【解决方案8】:

        我认为正常情况下编译后会有一些代码。

        其实我觉得 goto 有时候很方便,虽然很难读。

        【讨论】:

          【解决方案9】:

          转到循环:

          start_Chicken:
          {
              ++x;
              if (x >= loops)
                  goto end_Chicken;
          }
          goto start_Chicken;
          
          end_Chicken:
          x = 0;
          

          for循环:

          for (int i = 0; i < loops; i++)
          {
          
          }
          

          while 循环:

          while (z <= loops)
          {
          
              ++z;
          }
          z = 0;
          

          Image from results

          虽然在任何情况下循环使用更多混合测试的结果都一样少但仍然更好。

          【讨论】:

          • 你必须小心以这种方式进行“并行”分析;有时一个在另一个之前的顺序会改变结果......
          【解决方案10】:

          在 Linux 上,我使用 g++ 和 clang++ 将以下代码编译成程序集。有关我如何做到这一点的更多信息,请参阅here。 (简短版:g++ -S -O3 filename.cppclang++ -S -O3 filename.cpp,还有一些您将在下面看到的组装 cmets 来帮助我。)

          结论/TL;DR 在底部。


          首先,我比较了label:gotodo {} while。您不能将 for () {} 循环与 this 进行比较(出于善意),因为 for 循环总是首先评估条件。这一次,只有在循环代码执行一次后才会评估条件。

          #include <iostream>
          
          void testGoto()
          {
            __asm("//startTest");
            int i = 0;
            loop:
            std::cout << i;
            ++i;
            if (i < 100)
            {
              goto loop;
            }
            __asm("//endTest");
          }
          
          #include <iostream>
          
          void testDoWhile()
          {
            __asm("//startTest");
            int i = 0;
            do
            {
              std::cout << i;
              ++i;
            }
            while (i < 100);
            __asm("//endTest");
          }
          

          在这两种情况下,无论gotodo {} while,每个编译器的程序集都是完全相同的:

          g++:

              xorl    %ebx, %ebx
              leaq    _ZSt4cout(%rip), %rbp
              .p2align 4,,10
              .p2align 3
          .L2:
              movl    %ebx, %esi
              movq    %rbp, %rdi
              addl    $1, %ebx
              call    _ZNSolsEi@PLT
              cmpl    $100, %ebx
              jne .L2
          

          叮当++:

              xorl    %ebx, %ebx
              .p2align    4, 0x90
          .LBB0_1:                                # =>This Inner Loop Header: Depth=1
              movl    $_ZSt4cout, %edi
              movl    %ebx, %esi
              callq   _ZNSolsEi
              addl    $1, %ebx
              cmpl    $100, %ebx
              jne .LBB0_1
          # %bb.2:
          

          然后我比较了label:gotowhile {}for () {}。这一次,条件在循环代码执行一次之前就被评估了。

          对于goto,我必须颠倒条件,至少是第一次。我看到了两种实现方式,所以我尝试了两种方式。

          #include <iostream>
          
          void testGoto1()
          {
            __asm("//startTest");
            int i = 0;
            loop:
            if (i >= 100)
            {
              goto exitLoop;
            }
            std::cout << i;
            ++i;
            goto loop;
            exitLoop:
            __asm("//endTest");
          }
          
          #include <iostream>
          
          void testGoto2()
          {
            __asm("//startTest");
            int i = 0;
            if (i >= 100)
            {
              goto exitLoop;
            }
            loop:
            std::cout << i;
            ++i;
            if (i < 100)
            {
              goto loop;
            }
            exitLoop:
            __asm("//endTest");
          }
          
          #include <iostream>
          
          void testWhile()
          {
            __asm("//startTest");
            int i = 0;
            while (i < 100)
            {
              std::cout << i;
              ++i;
            }
            __asm("//endTest");
          }
          
          #include <iostream>
          
          void testFor()
          {
            __asm("//startTest");
            for (int i = 0; i < 100; ++i)
            {
              std::cout << i;
            }
            __asm("//endTest");
          }
          

          如上所述,在所有四种情况下,无论goto 1 还是2、while {}for () {},每个编译器的程序集都是完全相同的,g++ 只有1 个可能毫无意义的小例外:

          g++:

              xorl    %ebx, %ebx
              leaq    _ZSt4cout(%rip), %rbp
              .p2align 4,,10
              .p2align 3
          .L2:
              movl    %ebx, %esi
              movq    %rbp, %rdi
              addl    $1, %ebx
              call    _ZNSolsEi@PLT
              cmpl    $100, %ebx
              jne .L2
          

          g++ 的例外:在 goto2 程序集的末尾,程序集添加了:

          .L3:
              endbr64
          

          (我认为这个额外的标签是从goto 1 的程序集中优化出来的。)不过我认为这完全无关紧要。

          叮当++:

              xorl    %ebx, %ebx
              .p2align    4, 0x90
          .LBB0_1:                                # =>This Inner Loop Header: Depth=1
              movl    $_ZSt4cout, %edi
              movl    %ebx, %esi
              callq   _ZNSolsEi
              addl    $1, %ebx
              cmpl    $100, %ebx
              jne .LBB0_1
          # %bb.2:
          

          结论/TL;DR:不,label:gotodo {} while 的任何可能的等效安排之间似乎没有任何区别、while {}for () {},至少在使用 g++ 9.3.0 和 clang++ 10.0.0 的 Linux 上。

          注意我这里没有测试breakcontinue;但是,鉴于在任何情况下为 4 中的每一个生成的汇编代码都是相同的,我只能假设它们对于 breakcontinue 将完全相同,尤其是因为程序集使用标签和跳转每个场景。

          为了确保正确的结果,我在过程中非常细致,还使用了 Visual Studio Code 的比较文件功能。

          【讨论】:

            猜你喜欢
            • 2014-01-20
            • 1970-01-01
            • 1970-01-01
            • 2011-10-20
            • 1970-01-01
            • 2011-01-29
            • 2011-01-18
            • 2010-10-07
            • 2018-11-08
            相关资源
            最近更新 更多