函数过早返回的效率


97

作为一个缺乏经验的程序员,我经常遇到这种情况,尤其是对于我要优化的一个雄心勃勃,速度密集的项目,我感到特别奇怪。对于主要的类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的答案非常简洁地解释了为什么使用a return几乎可以忽略不计(在汇编中,return到函数末尾创建一个“分支”,这非常快。分支会更改PC寄存器和可能还会影响缓存和管道,这是很小的。)特别是在这种情况下,它实际上没有任何区别,因为if/else和和都return创建了到函数末尾的相同分支。


22
我认为这类事情不会对性能产生明显影响。只需编写一个小测试,看看自己。Imo,第一个变种更好,因为您不需要不必要的嵌套,从而提高了可读性
SirVaulterScoff 2011年

10
@SirVaulterScott,除非这两种情况在某种程度上是对称的,在这种情况下,您希望通过将它们置于相同的缩进级别来展现对称性。
luqui 2011年

3
SirVaulterScoff:+1减少了不必要的嵌套
fjdumont 2011年

11
可读性>>>微优化。以哪种方式对将要维持此状态的湿件更有意义。在机器代码级别上,即使将其馈送到相当笨的编译器中,这两个结构也是相同的。一个优化的编译器将消除两者之间任何类似的速度优势。
SplinterReality

12
不要担心此类事情来优化“速度密集型”项目。剖析您的应用程序,以找出它实际运行缓慢的地方-完成工作后,如果它实际上运行太慢。您几乎可以肯定无法猜测到底是什么在减慢速度。
blueshift,

Answers:


92

完全没有区别:

=====> 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
=====> 

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


59
或更好:某个编译器的至少一个版本为两个版本生成相同的代码。
UncleZeiv

11
@UncleZeiv-大多数(如果不是全部)编译器会将源代码转换为执行流程图模型。很难想象有一个理智的实施方式会为这两个示例提供有意义的不同流程图。您可能会看到的唯一区别是,两种不同的做事被交换了,甚至在许多实现中都无法撤消,以优化分支预测或针对平台确定首选排序的其他问题。
Steve314

6
@ Steve314,当然,我只是在挑剔:)
UncleZeiv

@UncleZeiv:也在clang上进行了测试,结果相同
Dani

我不明白 看来something()将始终执行。在原始问题中,OP具有Do stuffDo diffferent stuff取决于标志。我不知道生成的代码将是相同的。
Luc M

65

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

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

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


8
@Philip:也请大家帮个忙,不要再为此担心了。您编写的代码也将被其他人读取和维护(即使您编写的代码永远也不会被其他人读取,您仍然会养成习惯,这会影响您编写的其他代码并会被其他人读取)。始终编写代码,以使其易于理解。
hlovdal

8
优化器并不比您聪明!!!他们只能更快地确定影响在哪里没有太大关系。在真正重要的地方,您一定会比某些编译器具有更好的优化经验。
johannes

10
@johannes让我不同意。编译器不会改变您的算法以获得更好的算法,但是它在重新排序指令以实现最大的流水线效率以及其他对于循环(裂变,融合等)而言并非微不足道的事情上做得非常出色,即使是经验丰富的程序员也无法决定除非他对CPU架构有深入的了解,否则最好是先验的。
fortran

3
@johannes-对于这个问题,您可以假设确实如此。同样,通常,在某些特殊情况下,您有时可能比编译器更好地进行优化,但这几天需要大量的专业知识-通常情况是,优化器会应用您能想到的大多数优化,并且这样做系统地,而不仅仅是少数特殊情况。关于这个问题,编译器可能会为两种形式构造完全相同的执行流程图。选择更好的算法是一项艰巨的工作,但是代码级优化几乎总是在浪费时间。
Steve314,2011年

4
我同意和不同意。在某些情况下,编译器无法知道某物等同于其他物。您知道吗,这通常x = <some number>if(<would've changed>) x = <some number>未完成分支机构真正造成伤害的方法要快得多。另一方面,除非这是在非常密集的操作的主循环内,否则我也不必担心。
2011年

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;
}

上面的代码代表了过早返回的风格的极端版本​​。请注意,随着复杂性的增加,代码会随着时间变得非常重复且不可维护。如今,人们可能会使用异常处理来捕获这些异常

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示例之后,Philip建议在上面的catch块内使用一个不间断的开关/外壳。可以先切换(typeof(e))然后掉入free_resourcex()调用,但这并不容易,需要进行设计考虑。请记住,没有中断的开关/外壳与下面带有菊花链标签的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为内核代码实施了与资源相关的另一种样式,甚至以绝对合理的方式使用了臭名昭著的goto

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语句上“首选”的语言功能都是隐式的gotos,例如巨大的树状if / else,异常处理程序,loop / break / continue语句等。在上面的示例中,goto被认为是可以的,因为它们仅跳跃了一小段距离,具有清晰的标签,并且释放了其他混乱的代码来跟踪错误情况。这个问题也在stackoverflow上进行了讨论

但是,最后一个示例中缺少的是一种返回错误代码的好方法。我当时想result_code++在每次free_resource_x()调用后添加一个,然后返回该代码,但这抵消了上述编码样式的一些速度提升。如果成功,则很难返回0。也许我只是没有想象力;-)

所以,是的,我确实认为在编码过早返回与否之间存在很大的差异。但是我也认为,只有在更复杂或难以为编译器进行重构和优化的更复杂的代码中,这种情况才显而易见。一旦资源分配起作用,通常就是这种情况。


1
哇,真有趣。我绝对可以赞赏天真的方法的不可维护性。但是,异常处理将如何在特定情况下得到改进?就像在错误代码中catch包含无休止的switch声明吗?
菲利普·吉恩

@Philip添加了基本的异常处理示例。请注意,只有goto可能会掉线。您建议的switch(typeof(e))会有所帮助,但并非不重要,需要设计考虑。并且请记住,不间断的开关/外壳与带有菊花链标签的goto完全一样;-)
cfi

+1这是C / C ++(或需要手动释放内存的任何语言)的正确答案。就个人而言,我不喜欢多标签版本。在我以前的公司中,它始终是“ goto fin”(这是一家法国公司)。最后,我们将取消分配任何内存,这是goto唯一可以通过代码审查的用途。
基普(Kip)

1
请注意,在C ++中,您不会执行任何这些方法,但会使用RAII来确保正确清理资源。
马克B:

12

即使答案不多,生产编译器在优化方面也将比您做的更好。与这些类型的优化相比,我更倾向于可读性和可维护性。


9

具体来说,return它将被编译到方法末尾的一个分支中,在该分支中将有一条RET指令或可能存在的任何指令。如果省略它,则之前的代码块末尾else将被编译为一个到该代码else块末尾的分支。因此,您可以看到在这种特定情况下,它没有任何区别。


知道了 我实际上认为这很简洁地回答了我的问题。我想这实际上只是一个寄存器加法,几乎可以忽略不计(除非您可能正在进行系统编程,即使如此...)我要在此提一个荣誉。
菲利普·吉恩

@Philip还要注册什么?路径中根本没有多余的指令。
洛恩侯爵,

嗯,两个都将添加寄存器。这就是组装分支的全部,不是吗?除了程序计数器?我在这里可能是错的。
菲利普·金

1
@Philip不,程序集分支是程序集分支。它会影响课程的PC,但它可能是通过完全重新加载它,它也有在处理器的副作用WRT流水线,高速缓存等
洛恩侯爵

4

如果您真的想知道特定编译器和系统的编译代码是否有所不同,则必须自己编译并查看程序集。

但是,在大型方案中,几乎可以肯定的是,编译器的优化要比您的微调更好,即使不能这样做,对程序性能的影响也不大可能。

相反,应以最清晰的方式编写代码,以供人类阅读和维护,并让编译器执行其最擅长的工作:从源代码中生成最佳汇编。


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
    }
}

当然,一个函数的长度不应超过一页(甚至两页)。但是调试方面还没有包含在其他答案中。点了!
cfi

3

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

这将意味着使用反编译器或查看低级编译器输出(例如,汇编语言)。使用C#或任何.Net语言,此处记录工具将为您提供所需的工具

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


1

摘自Clean Code:《敏捷软件技巧手册》

标志参数很丑陋。将布尔值传递给函数是一种非常糟糕的做法。它立即使该方法的签名复杂化,大声宣称此函数可以完成多项任务。如果标志为true,则执行一件事,如果标志为false,则另一件事!

foo(true);

代码中的内容只会使读者导航到该函数,而浪费时间阅读foo(boolean flag)

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


我仅以此为例。传递给该函数的可能是一个int,double,一个类,您可以将其命名,这并不是问题的实质。
菲利普·吉恩

您提出的问题是关于在函数内部进行切换的,在大多数情况下,这是代码的味道。它可以通过多种方式实现,而读者不必阅读整个函数,而是说foo(28)是什么意思?

0

一个想法(目前还不记得提议它的人)是,从结构的角度来看,所有函数都应该只有一个返回点,以使代码更易于阅读和调试。我想这更多是为了进行宗教辩论。

您可能希望控制函数何时以及如何退出以打破该规则的一个技术原因是在编写实时应用程序时,并且您要确保通过函数的所有控制路径都需要完成相同数量的时钟周期。


嗯,我认为这与清理工作有关(尤其是用C进行编码时)。
Thomas Eding

不,无论您将方法留在何处,只要您返回堆栈,堆栈都会向下碰撞(即“清除”的所有内容)。
MartyTPS

-4

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


3
我可以说您在开玩笑,但可怕的是有些人可能只是认真对待您的建议!
Daniel Pryden 2011年

同意丹尼尔。尽管我很喜欢犬儒主义-不应在技术文档,白皮书和诸如此类的问答网站中使用它。
cfi

1
-1为愤世嫉俗的答案,不一定是初学者所能识别的。
Johan Bezem
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.