function过早返回的效率

这是我经常遇到的一个缺乏经验的程序员的情况,我特别想知道我正在试图优化的一个雄心勃勃,高速度的项目。 对于主要的类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 ? 如何涉及到堆栈? 有没有优化特殊情况? 是否有任何因素(如内联或“做什么”的大小)可能会显着影响这一点?

我总是支持提高可读性,而不是轻微的优化(我在参数validation中看到foo1),但是这样频繁出现,我想一劳永逸地放下所有的担心。

而且我意识到过早优化的陷阱…呃,那些是一些痛苦的回忆。

编辑:我接受了一个答案,但EJP的答案很简洁地解释了为什么使用return几乎可以忽略不计(在汇编中, return创build了一个'分支'到函数结束,这是非常快速的分支改变PC注册,也可能会影响caching和pipe道,这是非常微不足道的。)对于这种情况下,特别是,它没有区别,因为无论是if/elsereturn创build相同的分支到函数的结尾。

完全没有区别:

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

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

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

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

如果你想看看会发生什么,可以通过优化来构build这些代码,然后查看汇编器输出。

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

如果上面的简单例子被扩展了资源分配,然后错误检查可能导致释放资源,图片可能会改变。

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

 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处理来捕捉这些exception

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

Philip在看了下面的goto例子之后,build议在上面的catch块中使用一个无断开关的开关/shell 。 可以切换(typeof(e))然后通过free_resourcex()调用,但是这不是微不足道的,需要devise考虑 。 请记住,没有中断的开关/shell就像下面的菊花链标签一样。

正如Mark B指出的那样,在C ++中,遵循资源获取是初始化原则(简称RAII)被认为是一种很好的风格。 这个概念的要点是使用对象实例来获取资源。 只要对象超出范围并调用析构函数,资源就会自动释放。 对于相互依赖的资源,必须特别注意确保正确的释放顺序,并devise对象的types,以便所有的析构函数都可以使用所需的数据。

或者在例外的情况下可能会这样做:

 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,exception处理程序,循环/ break / continue语句等,而且上面的例子中的goto被认为是正确的,因为它们只是跳了一小段距离,有清晰的标签,并且释放了其他杂乱的代码来跟踪错误情况。 这个问题也在这里讨论了stackoverflow 。

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

所以,是的,我认为在过早回报编码问题上存在很大差异。 但是我也认为,只有在更复杂的代码中,对于编译器来说更难或不可能进行重构和优化。 一旦资源分配开始,通常就是这种情况。

尽pipe这不是一个很好的答案,但是生产编译器在优化方面会比现在更好。 我赞成在这些优化方面的可读性和可维护性。

具体来说, return将被编译到一个分支到方法的末尾,在那里将会有一条RET指令或者其它的指令。 如果省略, else之前的块的结尾将被编译到else块的结尾处。 所以你可以看到在这个特定的情况下,没有任何区别。

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

然而,在大的scheme中,几乎可以肯定的是,编译器可以比你的微调更好地进行优化,即使它不能,对你的程序的性能来说也不太重要。

相反,以最清晰的方式编写代码供人类阅读和维护,并让编译器执行最好的操作:从源代码生成最好的程序集。

在你的例子中,回报是显而易见的。 当返回是一个或两个页面上方/下方的/ /发生不同的事情发生时,debugging人员会发生什么? 当有更多的代码时,很难find/看到。

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

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

这将意味着使用反编译器或查看低级编译器输出(例如汇编语言)。 在C#或任何.Net语言中, 这里logging的工具将给你你所需要的。

但正如你自己所观察到的,这可能是不成熟的优化。

从清洁代码:敏捷软件工艺手册

国旗论点是丑陋的。 将布尔函数传递给函数是一个非常糟糕的做法。 它立即使方法的签名复杂化,大声宣告这个函数不止一件事。 如果国旗是真的,而国旗是假的,则是一样的。

 foo(true); 

在代码将只是让读者导航到function和浪费时间阅读foo(布尔标志)

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

一个思想stream派(不记得现在提出这个问题的人)是从结构的angular度来看,所有的函数应该只有一个返回点,使得代码更容易阅读和debugging。 我想,这更多是为了编制宗教辩论。

您可能想要控制何时以及如何退出function的一个技术原因违反了这一规则,即当您编写实时应用程序时,并且要确保通过该function的所有控制path都采用相同数量的时钟周期来完成。

我很高兴你提出这个问题。 你应该总是使用分支机构提前回报。 为什么要到那里? 如果可以的话,将所有function合并成一个(至less尽可能多)。 这是可行的,如果没有recursion。 最后,你会有一个巨大的主要function,但这是你需要/想要的这种事情。 之后,将您的标识符重新命名为尽可能短。 当你的代码执行的时候,读取名字的时间就会减less。 下一步做…