功能中的过早返回效率

时间:2011-10-25 04:26:45

标签: c# java c++ c compiler-optimization

这是我作为一个经验不足的程序员经常遇到的情况,我特别想知道我正在努力优化的一个雄心勃勃,速度密集的项目。对于主要的类C语言(C,objC,C ++,Java,C#等)及其常用的编译器,这两个函数是否同样有效运行?编译代码有什么不同吗?

void foo1(bool flag)
{
    if (flag)
    {
        //Do stuff
        return;
    }

    //Do different stuff
}

void foo2(bool flag)
{
    if (flag)
    {
        //Do stuff
    }
    else
    {
        //Do different stuff
    }
}

基本上,breakreturn早期是否有直接的效率奖励/惩罚?堆栈框架是如何涉及的?是否有优化的特殊情况?是否有任何因素(例如内联或“做事”的大小)可能会对此产生重大影响?

我总是支持改进可读性而非次要优化(我通过参数验证看到foo1很多),但这种情况经常出现,我想一劳永逸地抛开所有的担忧。

我知道过早优化的陷阱......呃,这些都是一些痛苦的回忆。

编辑:我接受了答案,但EJP的回答非常简洁地解释了为什么return的使用几乎可以忽略不计(在汇编中,return在函数末尾创建了一个“分支”这是非常快的。分支改变了PC寄存器,也可能影响缓存和流水线,这是非常小的。)特别是对于这种情况,它实际上没有区别,因为if/else和{{ 1}}在函数末尾创建相同的分支。

11 个答案:

答案 0 :(得分:91)

完全没有区别:

=====> cat test_return.cpp
extern void something();
extern void something2();

void test(bool b)
{
    if(b)
    {
        something();
    }
    else
        something2();
}
=====> cat test_return2.cpp
extern void something();
extern void something2();

void test(bool b)
{
    if(b)
    {
        something();
        return;
    }
    something2();
}
=====> rm -f test_return.s test_return2.s
=====> g++ -S test_return.cpp 
=====> g++ -S test_return2.cpp 
=====> diff test_return.s test_return2.s
=====> rm -f test_return.s test_return2.s
=====> clang++ -S test_return.cpp 
=====> clang++ -S test_return2.cpp 
=====> diff test_return.s test_return2.s
=====> 

意味着即使没有在两个编译器中进行优化,生成的代码也没有区别

答案 1 :(得分:65)

简短的回答是,没有区别。帮自己一个忙,不要再担心了。优化编译器几乎总是比你聪明。

专注于可读性和可维护性。

如果你想看看会发生什么,请在优化的基础上构建它们并查看汇编器输出。

答案 2 :(得分:28)

有趣的答案:虽然我同意所有这些答案(到目前为止),但这个问题的内涵可能会被完全忽视。

如果上面的简单示例是通过资源分配扩展的,然后进行错误检查,可能会导致资源释放,那么图片可能会发生变化。

考虑初学者可能采取的天真的方法

int func(..some parameters...) {
  res_a a = allocate_resource_a();
  if (!a) {
    return 1;
  }
  res_b b = allocate_resource_b();
  if (!b) {
    free_resource_a(a);
    return 2;
  }
  res_c c = allocate_resource_c();
  if (!c) {
    free_resource_b(b);
    free_resource_a(a);
    return 3;
  }

  do_work();

  free_resource_c(c);
  free_resource_b(b);
  free_resource_a(a);

  return 0;
}

以上将代表过早返回风格的极端版本​​。注意当代码的复杂性增加时,代码如何变得非常重复且不可维护。如今,人们可能会使用 exception handling 来抓住这些内容。

int func(..some parameters...) {
  res_a a;
  res_b b;
  res_c c;

  try {
    a = allocate_resource_a(); # throws ExceptionResA
    b = allocate_resource_b(); # throws ExceptionResB
    c = allocate_resource_c(); # throws ExceptionResC
    do_work();
  }  
  catch (ExceptionBase e) {
    # Could use type of e here to distinguish and
    # use different catch phrases here
    # class ExceptionBase must be base class of ExceptionResA/B/C
    if (c) free_resource_c(c);
    if (b) free_resource_b(b);
    if (a) free_resource_a(a);
    throw e
  }
  return 0;
}

菲利普建议,在查看下面的goto示例后,在上面的catch块中使用无断开关/ case 。可以切换(typeof(e))然后通过free_resourcex()调用,但这是not trivial and needs design consideration。请记住,没有中断的开关/盒子就像下面带有菊花链标签的goto ......

正如Mark B所指出的那样,在C ++中,遵循资源获取是初始化原则,简称RAII被认为是好的风格。该概念的要点是使用对象实例化来获取资源。一旦对象超出范围并调用其析构函数,资源就会自动释放。对于相互依存的资源,必须特别注意确保正确的解除分配顺序,并设计对象类型,以便所有析构函数都能获得所需的数据。

或者在例外日期间可能会这样做:

int func(..some parameters...) {
  res_a a = allocate_resource_a();
  res_b b = allocate_resource_b();
  res_c c = allocate_resource_c();
  if (a && b && c) {   
    do_work();
  }  
  if (c) free_resource_c(c);
  if (b) free_resource_b(b);
  if (a) free_resource_a(a);

  return 0;
}

但是这个过度简化的示例有几个缺点:只有在分配的资源不相互依赖的情况下才能使用它(例如,它不能用于分配内存,然后打开文件句柄,然后从句柄读取数据)进入内存),它不提供单独的,可区分的错误代码作为返回值。

保持代码快速(!),紧凑,易于阅读和扩展Linus Torvalds enforced a different style for kernel code that deals with resources, even using the infamous goto in a way that makes absolutely sense

int func(..some parameters...) {
  res_a a;
  res_b b;
  res_c c;

  a = allocate_resource_a() || goto error_a;
  b = allocate_resource_b() || goto error_b;
  c = allocate_resource_c() || goto error_c;

  do_work();

error_c:
  free_resource_c(c);
error_b:
  free_resource_b(b);
error_a:
  free_resource_a(a);

  return 0;
}

关于内核邮件列表的讨论的要点是,大多数语言功能都比goto语句“首选”是隐式的,例如巨大的,类似树的if / else,异常处理程序,循环/中断/继续上面例子中的goto被认为是好的,因为它们只跳了一小段距离,有清晰的标签,并且可以释放其他杂乱的代码来跟踪错误情况。 This question has also been discussed here on stackoverflow

然而,上一个示例中缺少的是返回错误代码的好方法。我想在每个result_code++调用之后添加free_resource_x()并返回该代码,但这会抵消上述编码风格的一些速度提升。如果成功,很难返回0。也许我只是缺乏想象力; - )

所以,是的,我确实认为编码过早回报的问题存在很大差异。但我也认为只有在更复杂的代码中才能显示出更难或不可能重构和优化编译器。一旦资源分配发挥作用,通常就是这种情况。

答案 3 :(得分:12)

即使这不是一个很好的答案,生产编译器在优化方面要比你好得多。我赞成这种优化的可读性和可维护性。

答案 4 :(得分:9)

具体而言,return将被编译到方法末尾的分支中,其中将有RET指令或其任何可能的指令。如果将其遗漏,则else之前的块结尾将被编译到else块末尾的分支中。所以你可以在这个特定情况下看到它没有任何区别。

答案 5 :(得分:4)

如果您真的想知道特定编译器和系统的编译代码是否存在差异,您必须自己编译并查看程序集。

然而,在大的方案中,几乎可以肯定编译器可以比你的微调更好地进行优化,即使它不能,它也不太可能对你的程序的性能有任何影响。

相反,以最清晰的方式编写代码供人类阅读和维护,让编译器尽其所能:从源代码中生成最佳汇编。

答案 6 :(得分:4)

在您的示例中,返回值很明显。当返回是一个或两个上/下的页面时,调试的人会发生什么?//发生不同的事情?当有更多代码时,很难找到/看到。

void foo1(bool flag)
{
    if (flag)
    {
        //Do stuff
        return;
    }

    //Do different stuff
}

void foo2(bool flag)
{
    if (flag)
    {
        //Do stuff
    }
    else
    {
        //Do different stuff
    }
}

答案 7 :(得分:3)

我非常同意blueshift:可读性和可维护性!但是如果你真的很担心(或者只是想了解你的编译器在做什么,从长远来看这绝对是一个好主意),你应该自己寻找。

这将意味着使用反编译器或查看低级编译器输出(例如汇编语言)。在C#或任何.Net语言中,tools documented here将为您提供所需的内容。

但正如你自己所观察到的,这可能是过早的优化。

答案 8 :(得分:1)

来自Clean Code: A Handbook of Agile Software Craftsmanship

  

Flag参数很难看。将布尔值传递给函数是一种非常糟糕的做法。它   立即使方法的签名复杂化,大声宣布这个功能   做不止一件事。如果标志为真,则执行一项操作,如果标志为假,则执行另一项操作!

foo(true);
代码中的

只会让读者导航到函数并浪费时间阅读foo(布尔标志)

更好的结构化代码库将为您提供更好的优化代码的机会。

答案 9 :(得分:0)

一个思想流派(不记得目前提出它的那个人)是从结构的角度来看所有函数应该只有一个返回点,以使代码更容易阅读和调试。我想,这更适合编写宗教辩论。

您可能想要控制函数退出的时间和方式违反此规则的一个技术原因是当您编写实时应用程序并且您希望确保通过该函数的所有控制路径采用相同数量的时钟周期时完成。

答案 10 :(得分:-4)

我很高兴你提出这个问题。你应该在早期返回时使用分支。为何停在那里?如果可以的话,将所有功能合并为一个(至少尽可能多)。如果没有递归,这是可行的。最后,你将拥有一个巨大的主要功能,但这就是你需要/想要的东西。然后,将您的标识符重命名为尽可能短。这样,在执行代码时,花在阅读名称上的时间就会减少。接下来做......