代码之家  ›  专栏  ›  技术社区  ›  dsimcha

C尾部调用优化

  •  30
  • dsimcha  · 技术社区  · 14 年前

    我经常听到人们说C不执行尾部调用消除。尽管标准没有保证,但在实践中,它不是通过任何体面的实现来实现的吗?假设您只针对成熟、实现良好的编译器,而不关心为晦涩难懂的平台编写的原始编译器的绝对最大可移植性,那么在C中依赖尾部调用消除是否合理?

    8 回复  |  直到 14 年前
        1
  •  29
  •   AnT stands with Russia    11 年前

    像“C不执行尾部调用消除”这样的语句毫无意义。正如您自己正确指出的,类似这样的事情完全取决于实现。是的,任何合适的实现都可以很容易地将尾部递归变成一个循环。当然,C编译器通常不会对每段特定代码中会发生什么样的优化以及不会发生什么样的优化给出任何保证。你得自己编译看看。

        2
  •  7
  •   Dobes Vandermeer Tahbaza    11 年前

    尽管现代编译器可能会在启用优化的情况下进行尾部调用优化,但是调试构建可能会在没有它的情况下运行,这样您就可以获得堆栈跟踪、代码的步进/步出以及诸如此类的美妙事情。在这种情况下,不需要尾部调用优化。

    由于尾部调用优化并不总是可取的,因此将其委托给编译器编写者是没有意义的。

        3
  •  5
  •   Theodore Murdock    9 年前

    我认为尾部调用优化只需要在 很多 预期的或需要的递归类型;也就是说,在鼓励或实施函数式编程风格的语言中(有了这些语言,你会发现 for while 循环要么被强烈劝阻,要么被认为是不雅观的,甚至可能完全不存在于语言中,因此出于所有这些原因,您可能会求助于递归,甚至可能更多。)

    C编程语言(IMHO)显然是 对于 do .. while ,

    将此与没有 虽然 需要 递归;这反过来意味着语言必须确保,经过多次迭代,堆栈溢出不会成为问题;因此这种语言的官方标准 可以


    附笔。: 对于

        4
  •  3
  •   Frank    14 年前

    回答最后一个问题:标准绝对不应该对优化做任何声明。在有些环境中,实施起来或多或少有点困难。

        5
  •  1
  •   bta    14 年前

    语言标准定义了语言的行为方式,而不是编译器的实现方式。优化不是强制性的,因为它并不总是需要的。编译器提供了一些选项,用户可以在需要时启用优化,也可以关闭优化。编译器优化可能会影响调试代码的能力(以逐行方式将C与程序集匹配变得更加困难),因此仅在用户请求时执行优化是有意义的。

        6
  •  1
  •   Evan Benn    6 年前

    对于那些谁喜欢证明建设,这里是godbolt做了一个很好的尾部调用优化和内联: https://godbolt.org/z/DMleUN

    但是,如果您将优化设置为-O3(或者如果您等待几年或使用不同的编译器,则毫无疑问),那么优化将完全消除循环/递归。

    下面是一个即使使用-O2也可以优化为单个指令的示例: https://godbolt.org/z/CNzWex

        7
  •  0
  •   nondeterministic    7 年前

    有些情况下,尾部调用优化可能会破坏ABI,或者至少很难以语义保持的方式实现。例如,想想共享库中的位置无关代码:当各种不同的应用程序都依赖于相同的功能时,一些平台允许程序与库动态链接以节省主内存。在这种情况下,库被加载一次并映射到每个程序的虚拟内存中,就好像它是系统上唯一的应用程序一样。在UNIX和其他一些系统上,这是通过对库使用位置无关的代码来实现的,因此寻址是相对于偏移量的,而不是相对于固定地址空间的绝对值。然而,在许多平台上,与位置无关的代码不能进行尾部调用优化。所涉及的问题是,在程序中导航的偏移量必须保存在寄存器中;在Intel 32位上, %ebx 它是被叫方保存的寄存器;其他平台也遵循这一理念。与使用普通调用的函数不同,那些部署尾部调用的函数必须在分支到子例程之前还原被调用方保存的寄存器,而不是在它们返回自身时。通常,这是没有问题的,因为此时,最顶层的调用函数并不关心存储在 %ebx公司 ,但与位置无关的代码依赖于每个跳转、调用或分支命令的该值。

    其他问题可能在面向对象语言(C++)中清理干净,这意味着函数中的最后一次调用实际上不是最后一次调用——清理是。因此,在这种情况下,编译器通常不会进行优化。

    阿尔索 setjmp longjmp 当然,这是有问题的,因为这实际上意味着函数可以在实际完成之前多次完成执行。很难或不可能在编译时优化!

        8
  •  0
  •   supercat    6 年前

    编译器通常会识别函数在调用另一个函数后不需要执行任何操作的情况,并用跳转替换该调用。许多情况下,可以安全地做到这一点是很容易识别的,这样的情况有资格作为“安全低挂果实”。然而,即使是在能够执行这种优化的编译器上,当应该或将要执行优化时,也可能并不总是显而易见的。各种因素可能会使尾部呼叫的成本高于正常呼叫的成本,而且这些因素可能并不总是可预测的。例如,如果函数以 return foo(1,2,3,a,b,c,4,5,6); foo(a,b,c,d,e,f,g,h,i); 同样地。

    如果一种语言有一种特殊的“尾部调用”语法,要求给定的编译器尽可能地进行尾部调用,否则拒绝编译,那么代码可以安全地假定这样的函数可以嵌套任意深。然而,当使用普通的调用语法时,没有通用的方法可以知道编译器是否能够比“普通”编译器更便宜地执行尾部调用。