这是我作为一个没有经验的程序员经常遇到的情况,我特别想知道我正在尝试优化的一个雄心勃勃、速度密集的项目。对于主要的类 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
}
}
基本上,当break
或return
提早时,是否有直接的效率奖励/惩罚?堆栈帧是如何参与的?有优化的特殊情况吗?是否有任何因素(如内联或“Do stuff”的大小)可能会显着影响这一点?
我一直支持通过次要优化提高易读性(我在参数验证中经常看到 foo1),但这经常出现,以至于我想一劳永逸地抛开所有担忧。
而且我知道过早优化的陷阱......呃,那些是一些痛苦的记忆。
编辑:我接受了一个答案,但 EJP 的答案非常简洁地解释了为什么使用 return
几乎可以忽略不计(在汇编中,return
在函数末尾创建了一个“分支”,这非常快。分支改变了 PC 寄存器,也可能影响缓存和管道,这是非常微不足道的。)特别是对于这种情况,它实际上没有区别,因为 if/else
和 return
都创建相同的分支到功能。
根本没有区别:
=====> 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
=====>
这意味着即使没有在两个编译器中进行优化,生成的代码也没有任何区别
简短的回答是,没有区别。帮自己一个忙,不要再为此担心了。优化编译器几乎总是比你聪明。
专注于可读性和可维护性。
如果您想看看会发生什么,请使用优化构建这些并查看汇编器输出。
x = <some number>
通常比执行 if(<would've changed>) x = <some number>
快得多 不需要的分支确实会造成伤害。另一方面,除非这是在极其密集的操作的主循环中,否则我也不会担心。
有趣的答案:虽然我确实同意所有这些(到目前为止),但到目前为止,这个问题可能有一些含义被完全忽视。
如果上面的简单示例通过资源分配进行扩展,然后通过潜在的资源释放进行错误检查,则情况可能会发生变化。
考虑一下初学者可能会采取的幼稚方法:
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 示例后,Philip 建议在上面的 catch 块中使用 无中断开关/案例。可以 switch(typeof(e)) 然后通过 free_resourcex()
调用,但这是 not trivial and needs design consideration。请记住,没有中断的开关/外壳与下面带有菊花链标签的 goto 完全一样...
正如 Mark B 所指出的,在 C++ 中,遵循 Resource Aquisition is Initialization 原则(简而言之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 语句“首选”的语言特性都是隐式 goto,例如巨大的树状 if/else、异常处理程序、循环/中断/继续语句等. 上面例子中的 goto 被认为是没问题的,因为它们只跳了一小段距离,有清晰的标签,并且释放了其他杂乱的代码来跟踪错误情况。 This question has also been discussed here on stackoverflow。
但是,最后一个示例中缺少的是返回错误代码的好方法。我正在考虑在每个 free_resource_x()
调用之后添加一个 result_code++
并返回该代码,但这抵消了上述编码风格的一些速度增益。并且在成功的情况下很难返回 0。也许我只是缺乏想象力;-)
所以,是的,我确实认为是否编码过早返回的问题存在很大差异。但我也认为只有在更复杂的代码中才明显,这些代码更难或不可能为编译器重构和优化。一旦资源分配发挥作用,通常就是这种情况。
switch
语句的 catch
?
尽管这不是一个答案,但生产编译器在优化方面会比你好得多。我更倾向于这些优化的可读性和可维护性。
具体来说,return
将被编译到方法末尾的分支中,其中将有 RET
指令或任何可能的指令。如果将其省略,则 else
之前的块末尾将被编译为 else
块末尾的分支。因此,您可以看到在这种特定情况下它没有任何区别。
如果您真的想知道特定编译器和系统的编译代码是否存在差异,您必须自己编译并查看程序集。
然而,在大的计划中,几乎可以肯定编译器可以比你的微调更好地优化,即使它不能,它也不太可能对你的程序性能产生影响。
相反,以最清晰的方式编写代码以供人类阅读和维护,并让编译器做它最擅长的事情:从您的源代码生成最好的程序集。
在您的示例中,回报是显而易见的。当返回是上面/下面的一两个页面时,调试人员会发生什么//做不同的事情?当有更多代码时,更难找到/查看。
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 语言中,tools documented here 将为您提供所需的内容。
但正如您自己所观察到的,这可能是过早的优化。
来自Clean Code: A Handbook of Agile Software Craftsmanship
标志参数很丑陋。将布尔值传递给函数是一种非常糟糕的做法。它立即使方法的签名复杂化,大声宣称该函数不止一件事。如果标志是真的,它会做一件事,如果标志是假的,它会做另一件事!
foo(true);
在代码中只会让读者导航到函数并浪费时间阅读 foo(boolean flag)
更好的结构化代码库将为您提供更好的优化代码的机会。
一种思想流派(现在不记得提出它的蛋头了)是,从结构的角度来看,所有函数都应该只有一个返回点,以使代码更易于阅读和调试。我想,这更多是为了编写宗教辩论。
您可能想要控制违反此规则的函数何时以及如何退出的一个技术原因是,当您编写实时应用程序时,您希望确保通过该函数的所有控制路径花费相同数量的时钟周期来完成。
很高兴你提出这个问题。您应该始终使用分支而不是提前返回。为什么停在那里?如果可以(至少尽可能多),将所有功能合并为一个。如果没有递归,这是可行的。最后,您将拥有一个庞大的主要功能,但这就是您对这类事情的需要/想要的。之后,将您的标识符重命名为尽可能短。这样,当你的代码被执行时,花在读取名字上的时间就更少了。接下来做...
不定期副业成功案例分享
something()
将始终被执行。在原始问题中,OP 有Do stuff
和Do diffferent stuff
取决于标志。我不确定生成的代码是否相同。