功能中的过早返回效率

Phi*_*lip 96 c c# c++ java 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
    }
}
Run Code Online (Sandbox Code Playgroud)

基本上,是否有提前breakreturn早期的直接效率奖金/罚款?堆栈框架是如何涉及的?是否有优化的特殊情况?是否有任何因素(如内联或"Do stuff"的大小)可能会对此产生重大影响?

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

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

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

Dan*_*ani 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
=====> 
Run Code Online (Sandbox Code Playgroud)

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

  • 或者更好:至少有某个版本的某个编译器会为这两个版本生成相同的代码. (59认同)
  • @UncleZeiv - 大多数(如果不是所有)编译器都会将源转换为执行流图模型.很难想象一个理智的实现会为这两个例子提供*有意义的*不同的流程图.关于您可能看到的唯一区别是两个不同的do-somethings被交换 - 甚至在许多实现中可能会撤消以优化分支预测或平台确定首选排序的其他问题. (11认同)
  • @ Steve314,当然,我只是在挑剔:) (6认同)

blu*_*ift 65

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

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

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

  • @johannes让我不同意.编译器不会为了更好的算法而改变你的算法,但它在重新排序指令以实现最大流水线效率和其他不那么微不足道的事情(裂变,融合等)方面做得非常出色,即使是有经验的程序员也无法决定什么是更好的先验,除非他对CPU架构有深入的了解. (10认同)
  • @Philip:并且帮助其他人,不要再为此担心了.你编写的代码也会被其他人阅读和维护(甚至你应该写,其他人永远不会阅读,你仍然会养成习惯,影响你编写的其他代码会被他人阅读).*始终*编写代码尽可能容易理解. (8认同)
  • 优化器并不比你聪明!他们只能更快地决定影响无关紧要的地方.在真正重要的地方,您肯定会有一些经验优于编译器的优化. (8认同)
  • 我同意并不同意这一点.有些情况下,编译器无法知道某些东西等同于其他东西.您是否知道`x = <某些数字>`比`if(<will've changed>)x = <某些数字>`通常更快更快``不需要的分支真的会受到伤害.另一方面,除非这是在极其密集的操作的主循环内部,否则我也不会担心. (4认同)
  • @johannes - 对于这个问题,你可以假设它.此外,一般情况下,您可能*偶尔*能够在一些特殊情况下优于编译器进行优化,但这些日子需要相当多的专业知识 - 通常情况下,优化器应用您可以想到的大多数优化和系统地这样做,而不仅仅是在一些特殊情况下.WRT这个问题,编译器可能会为*两个*形式构造*精确*相同的执行流程图.选择更好的算法是一项人类工作,但代码级优化几乎总是浪费时间. (3认同)

cfi*_*cfi 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;
}
Run Code Online (Sandbox Code Playgroud)

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

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;
}
Run Code Online (Sandbox Code Playgroud)

在看了下面的goto示例之后,Philip建议在上面的catch块中使用一个无断开关/ case.可以切换(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;
}
Run Code Online (Sandbox Code Playgroud)

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

为了保持代码快速(!),紧凑,易于阅读和可扩展,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;
}
Run Code Online (Sandbox Code Playgroud)

关于内核邮件列表的讨论要点是,与goto语句相比"首选"的大多数语言特性都是隐式的,例如巨大的,类似树的if/else,异常处理程序,循环/中断/继续语句等等.上面例子中的goto被认为是好的,因为它们只是跳跃一小段距离,有清晰的标签,并且可以释放其他杂乱的代码来跟踪错误情况.这个问题也在stackoverflow上讨论过.

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

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


Lou*_*Lou 12

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


use*_*421 9

具体而言,return将被编译到方法末尾的分支中,其中将存在RET指令或其可能的任何指令.如果你把它留出来,那么块之前的块else将被编译成一个分支到else块的末尾.所以你可以在这个特定情况下看到它没有任何区别.