C/C++:使用GOTO比WHILE和FOR更快吗?

19
我知道,大家都很讨厌GOTO,也不推荐使用它。但这并不是重点。我只是想知道哪个代码最快:
  1. 使用 goto 的循环

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

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

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

    8
    你已经编写好了所有的代码...为什么不对其进行性能分析呢? - Brian Roach
    2
    @Arturr,这并没有一个通用的答案。由于循环在功能上是相同的,所以你的问题恰恰是关于编译器会对它们做什么的。正如Gabe在下面所说的那样,你应该期望一个合理的编译器为所有三个循环提供相同的汇编代码。虽然不一定要这样做,但你可以编写一个使其中一个循环变得更慢的编译器,而没有任何好的理由。 - David German
    2
    很可能它们都会编译成相同的汇编代码,执行一个增量cmpjmp。或者(根据给定的示例)在任何高于-O0的级别上,它最终将直接调用printf() 3次。 - John Ledbetter
    2
    我曾经想给这个严重误导的问题打-1分,但那不是你的错。但现在看到你的态度,我要打-1分了。没关系。选择正确的方法,让编译器去完成它的工作,然后结束一天。如果有问题,根据你的分析器报告,查看汇编代码并尝试修复它,不要试图进行荒谬的概括。最后,goto不是坏的“因为某个聪明人说它是”,他之所以这么说是因为它是有问题的。你对反对使用goto的建议的轻率态度是没有根据的;有比goto更好的表达意图的方式,这是毋庸置疑的。 - GManNickG
    3
    在循环中加入 printf 的问题是可笑的。与循环控制相比,printf 会花费更多的时间。除非循环内有经过优化的代码(如无缓存失效等情况),否则处理循环控制本身的时间根本不值得考虑。 - Jens Gustedt
    显示剩余13条评论
    10个回答

    21

    一般来说,forwhile循环会被编译成与goto相同的代码,所以通常不会有区别。如果你怀疑,可以尝试使用这三个方法,并查看哪个需要更长时间。即使你循环了十亿次,很可能也无法测量出差异。

    如果你查看这个答案,你会看到编译器可以为forwhilegoto生成完全相同的代码(只是在这种情况下没有条件)。


    在所引用的答案中,“无条件”的部分在某些情况下可能对断言至关重要,否则goto版本可能会被翻译为一个有条件测试后跟着一个无条件分支,而不是直接的有条件分支。 - Clifford

    8

    编写简短的程序,然后执行以下步骤:

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

    分析输出并查看是否有差异。确保引入一定程度的不可预测性,以使编译器无法将程序优化为无内容。
    编译器在优化这些琐碎问题方面做得很好。我建议不要担心它,而是专注于成为更有生产力的程序员。
    速度和效率是值得关注的重要事项,但99%的时间都涉及使用适当的数据结构和算法...而不是担心一个for循环是否比while或goto快等。

    -O3会给你完全的优化。 - Andrew
    对于那些尝试这个的人,请查看我的答案这里,以获取有关如何为g++和clang++添加注释的更多信息,以便您知道要查看汇编的哪个部分... - Andrew
    @Andrew 那个和 -flto - user426

    7
    我曾经看到过有人在W. Richard Stevens的文章或书中提出了goto的论点。他的观点是,在代码的一个非常关键的部分(我相信他的例子是网络堆栈)中,使用具有相关错误处理代码的嵌套if/else块可以通过使用goto进行重新设计,从而产生有价值的差异。
    就个人而言,我不是一个足够优秀的程序员,无法与Stevens的工作争论,因此我不会尝试。goto对于性能相关问题确实有用,但在什么情况下使用的限制是相当严格的。

    1
    OP正在询问goto与循环结构的比较,而不是条件语句。 - Gabe
    是的...你的回答非常准确。我挖掘了(虽然只是从记忆中)我所见过的关于goto与其他结构的唯一相关讨论。我的最后一句话是这个“答案”的关键。我不指望它成为被接受的答案,但我相信它对于整个讨论是相关的。 - Harper Shelby
    那是一种非常好的实践,当您想要优化某些代码并且在某些极端情况下速度更重要时,我也会使用它。但无论如何,如果我能的话,我会为您的回答投一票。 - Artur Iwan

    3

    这可能与编译器、优化器和架构有关。

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

    最终,差异可能微乎其微,只有在需要大量迭代时才相关,并且你应该回答这个问题的方法是为特定目标和编译器(以及编译器设置)构建代码,然后检查生成的机器级别代码或直接测量执行时间。

    在你的示例中,循环中的printf()将在任何情况下都占据主导地位;在循环中使用更简单的东西将使观察到的差异更容易。我建议使用一个空循环,然后声明ivolatile,以防止循环被优化成空。


    2
    只要您生成与普通循环相同的控制流,几乎任何一个好的编译器都可以并且会为此生成相同的代码,无论您使用 for, while 等方式。
    使用 goto 可以获得一些优势,但通常只有在您生成的控制流不太规范(至少不是很干净)时才能发挥作用。一个典型的例子是跳到循环中间以获得一个半循环结构,大多数语言的普通循环语句(包括 C 语言的)都不能提供这种结构。

    2

    所有循环和goto之间不应该有任何显著差异。除了一个想法,编译器更可能根本不尝试优化GOTO事物。

    而且,试图优化循环中由编译器生成的内容并没有太多意义。更有意义的是优化循环内部的代码,或者减少迭代次数等。


    +1 针对这个观察的认识是,优化循环内部更为重要。 - Jens Gustedt

    1
    在Linux上,我使用g++和clang++将下面的代码编译成汇编。有关我如何做到这一点的更多信息,请参见此处。(简短版本:g++ -S -O3 filename.cpp clang++ -S -O3 filename.cpp,还有一些汇编注释,你将在下面看到,以帮助我理解。)
    结论/TL;DR在底部。

    首先,我比较了label:gotodo {} while。你不能(诚实地)将for () {}循环与此进行比较,因为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");
    }
    

    在这两种情况下,无论是使用goto还是do {} 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
    

    clang++:

        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++存在一些微小的例外,可能是无意义的:

    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的汇编代码。不过我认为这完全是无关紧要的。
    clang++:
        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:
    

    总之/简而言之:在Linux使用g++ 9.3.0和clang++ 10.0.0时,任何可能的等效安排label:gotodo {} whilewhile {}for () {}之间似乎没有任何区别。
    请注意,我在这里没有测试breakcontinue;但是,由于在任何情况下生成的4个汇编代码相同,因此我只能推测breakcontinue也将完全相同,特别是因为汇编正在为每种情况使用标签和跳转。
    为确保正确的结果,我非常细心地进行了处理,并使用了Visual Studio Code的比较文件功能。

    1

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

    事实上,我认为goto有时非常方便,尽管它很难读。


    0

    在一些特定的领域中,goto仍然被一些非常聪明的人广泛使用,并且在这些环境中并不存在对goto的偏见。我曾经在一个专注于模拟的公司工作过,那里的所有本地fortran代码都有大量的goto语句,团队非常聪明,软件几乎没有缺陷。

    所以,我们可以把goto的价值放在一边,如果问题仅仅是比较循环的话,我们可以通过分析和/或比较汇编代码来进行。然而,问题中包含了printf等语句。当进行这种操作时,你无法真正讨论循环控制逻辑的优化。此外,正如其他人指出的那样,给定的循环将会生成非常相似的机器代码。

    在流水线处理器架构中,所有条件分支在解码阶段之前都被认为是“成功”的(true),此外,小循环通常会被扩展为无循环。因此,与Harper上面的观点一致,在简单的循环控制中,goto不太可能有任何优势(就像for和while彼此之间也没有优势一样)。在多重嵌套循环或多重嵌套if语句中,只要将goto的附加条件添加到每个嵌套循环或嵌套if语句中都是次优的。

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

    0

    跳转到循环:

    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;
    

    结果中的图像

    在任何情况下,带有更多混合测试的while循环都具有最小但仍然更好的结果。


    在进行“并行”分析时,你必须小心;有时候一个在另一个之前的顺序会改变结果... - Andrew

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接