【问题标题】:Can't recursive functions be inlined? [duplicate]不能内联递归函数吗? [复制]
【发布时间】:2011-01-13 11:39:16
【问题描述】:

可能重复:
Can a recursive function be inline?

内联递归函数的权衡是什么。

【问题讨论】:

  • 您如何建议内联递归函数?通过使用inline 指令?
  • 递归模板函数算吗?

标签: c++ c


【解决方案1】:

一些编译器会将尾递归转换为普通循环,从而正常内联它们。

非尾递归可以内联到给定的深度,通常由编译器决定。

我从未遇到过这方面的实际应用,因为调用成本不再高到足以抵消代码大小的增加。

[编辑](澄清一下:尽管我喜欢玩弄这些东西,并且出于好奇经常检查我的编译器为“有趣的东西”生成了什么代码,但我还没有遇到过用例任何此类展开都有很大帮助。这并不意味着它们不存在或无法构建。

唯一有用的地方是在编译期间预先计算低迭代。但是,根据我的经验,这极大地增加了编译时间,而运行时性能优势通常可以忽略不计。

请注意,Visual Studio 2008(及更早版本)为您提供了相当多的控制权:

#pragma inline_recursion(on)
#pragma inline_depth(N)
__forceinline

小心后者,它很容易使编译器过载:)

【讨论】:

  • 哦,在紧循环中有实际应用。避免函数调用的好处可以非常明显地抵消增加的代码大小。
  • 康拉德,我添加了一个澄清。从我的特定 POV 来看,似乎选择合适的展开深度是真正困难的部分。
【解决方案2】:

当然可以声明为内联。 inline 关键字只是对编译器的提示。在许多情况下,编译器只是忽略它,根据编译器,这可能是这种情况之一。

【讨论】:

    【解决方案3】:

    当然。如果有意义,任何函数都可以内联:

    int f(int i)
    {
        if (i <= 0) return 1;
        else return i * f(i - 1);
    }
    int main()
    {
        return f(10);
    }
    

    伪汇编(f 在 main 中内联):

    main:
        mov r0, #10   ; Pass 10 to f
    f:
        cmp r0, #0    ; arg <= 0? ...
        bge 1l
        mov r0, #1    ; ... is so, return 1
        ret
    
     1:
        mov r0, -(sp) ; if not, save arg.
        dec r0        ; pass arg - 1 to f
        call f        ; just because it's inlined doesn't mean I can't call it.
        mul r0, (sp)+ ; compute the result
        ret           ; done.
    

    ;-)

    【讨论】:

    • 伪组装零件只是你炫耀的。但我必须承认我爱上了它。太酷了。
    • @Mark:我只是在炫耀。
    【解决方案4】:

    当您知道递归链在正常情况下不会那么长时,您可以内联到预定义的级别(我不知道,今天是否有任何现有的编译器足够智能)。

    内联递归函数很像展开循环。你最终会得到很多重复的代码——但在某些情况下它可能是值得的:

    • 递归调用的数量(链的长度)通常很短(如果它比预定义的长,只需进行正常递归)
    • 与逻辑相比,函数调用的开销相对较大——因此进行一些“展开”,例如五个实例并最终再次进行递归调用——这将节省 80% 的调用开销。
    • 当然是尾递归的特殊情况 - 但其他人提到了这一点。

    【讨论】:

      【解决方案5】:

      当您更改命令顺序执行顺序并跳转(调用或jmp)到函数所在的某个地址时调用普通函数。内联意味着你在这个函数的所有出现的地方放置这个函数的命令,所以你没有一个可以跳转的地方,也可以使用其他类型的优化,比如取消推送/弹出函数参数。

      【讨论】:

        【解决方案6】:

        [编辑:刚刚注意到虽然您的标题说“内联”,但您的实际问题是“使函数内联”。这两者实际上没有任何关系,他们只是有令人困惑的相似名称。在现代编译器中,inline 的主要作用是最初在 C99 中(我认为)只是使内联工作的必要细节:允许具有外部链接的符号的多个定义。这是因为现代编译器并没有非常关注程序员对函数是否应该内联的意见。不过,他们确实支付了一些费用,因此概念的混乱仍然存在。我已经回答了标题中的问题,这是编译器做出的决定,而不是正文中的问题,这是程序员做出的决定。]

        内联不一定是全有或全无的交易。编译器用来决定是否内联的一种策略是保持内联函数调用,直到结果代码“太大”。 “大”是由一些希望是明智的启发式定义的。

        所以考虑下面的递归函数(故意不是简单的尾递归):

        int triangle(int n) {
            if (n == 1) return 1;
            return n + triangle(n-1);
        }
        

        如果这样调用:

        int t100() {
            return triangle(100);
        }
        

        那么原则上没有什么特别的理由表明编译器用于内联的通常规则不应该导致这种情况:

        int t100() {
            // inline call to triangle(100)
            int result;
            if (100 == 1) { result = 1; } else {
                // inline call to triangle(99)
                int t99;
                if (100 - 1 == 1) { t99 = 1; } else {
                    // inline call to triangle(98)
                    int t98;
                    if (100 - 1 - 1 == 1) { t98 = 1; } else {
                        // oops, "too big", no more inlining
                        t98 = triangle(100 - 1 - 1 - 1) + 98;
                    }
                    t99 = t98 + 99;
                }
                result = t99 + 100;
            }
            return result;
        }
        

        很明显,优化器会很高兴,所以它比看起来“小”得多:

        int t100() {
            return triangle(97) + 297;
        }
        

        triangle 中的代码本身可以通过几级内联“展开”几步,方式完全相同,只是它没有常量的好处:

        int triangle(int n) {
            if (n == 1) return 1;
            if (n == 2) return 3;
            if (n == 3) return 6;
            return triangle(n-3) + 3*n - 3;
        }
        

        我怀疑编译器是否真的这样做了,不过,我想我从来没有注意到它[编辑:如果你告诉它,MSVC 会这样做,谢谢 peterchen]。

        在节省调用开销方面有一个明显的潜在好处,但人们并不真正期望递归函数内联,并且不能特别保证通常的内联启发式算法在递归函数(其中有两个不同的地方,调用站点和递归调用,可能是内联的,在每种情况下都有不同的好处)。此外,在编译时很难估计递归的深度,并且内联启发式可能会考虑调用深度来做出决策。所以编译器可能只是不打扰。

        函数式语言编译器通常很多比 C 或 C++ 编译器更积极地处理递归。相关的权衡是用函数式语言编写的这么多函数都是递归的,如果编译器无法优化尾递归,那么性能可能毫无希望。所以 Lisp 程序员通常依赖递归函数的良好优化,而 C 和 C++ 程序员通常不会。

        【讨论】:

        • visual studio 甚至为此提供了#pragmas:inline_recursioninline_depth。见msdn.microsoft.com/en-us/library/69hzy453.aspx。玩它真的很有趣,看看编译器做了什么。 +1 以获得深入的答案。
        • 可能会在某个时候这样做,看看优化器是否真的能想出triangle(100) -> triangle(92) + 772 正如我“预测”的那样:-)
        【解决方案7】:

        内联意味着在每次调用标记为内联的函数时,编译器都会将所述函数代码的副本放在那里。这避免了函数调用机制,并且它通常是参数堆栈推入弹出,在每秒大量调用的情况下节省时间。你看到静态变量和类似的东西的后果了吗?都过去了……

        因此,如果您有一个内联递归调用,那么您的编译器要么是超级智能的,并且可以确定副本的数量是否具有确定性,它会说“无法使其内联”,因为它不知道何时停止。

        【讨论】:

          【解决方案8】:

          现在,等一下。尾递归函数可以很容易地展开和内联。显然有编译器可以做到这一点,但我不知道具体情况。

          【讨论】:

            【解决方案9】:

            如果您的编译器不支持,您可以尝试手动内联...

            int factorial(int n) {
                int result = 1;
                if (n-- == 0) {
                    return result;
                } else {
                    result *= 1;
                    if (n-- == 0) {
                        return result;
                    } else {
                        result *= 2;
                        if (n-- == 0) {
                            return result;
                        } else {
                            result *= 3;
                            if (n-- == 0) {
                                return result;
                            } else {
                                result *= 4;
                                if (n-- == 0) {
                                    return result;
                                } else {
                                    // ...
                                }
                            }
                        }
                    }
                }
            }
            

            看到问题了吗?

            【讨论】:

              【解决方案10】:

              Tail recursion(递归的一种特殊情况)可以被 smart 编译器内联。

              【讨论】:

                【解决方案11】:

                可以通过尾递归优化的递归函数当然可以内联。如果一个函数做的最后一件事是调用自己,那么它可以被转换成一个普通的循环。

                【讨论】:

                • 可以说这是一个两步过程,但是:(1) 转换函数使其不再是递归的,(2) 内联非递归函数。并不是我特别想要这个论点,我只是想说明,即使一个递归函数没有从递归到迭代进行尾调用优化,它可能仍然在某些调用站点被内联,暴力和 -无知的方式。它只是不能在所有调用站点内联。
                • 非常正确 - 这与我的回答并不矛盾(我没有说尾端是唯一可以内联递归函数的方式!)
                【解决方案12】:

                不能内联任意递归函数,原因与蛇无法吞下自己的尾巴相同。

                【讨论】:

                • -1,误导类比和错误。仍然可以替换函数调用指令,但您现在将其替换为跳转。
                • @MSalters:正常跳转是不够的,因为您仍然需要跟踪状态。做你自己的推送和弹出堆栈会让事情变得更糟......
                • 我假设编译器知道堆栈是如何工作的......
                猜你喜欢
                • 1970-01-01
                • 2010-09-16
                • 1970-01-01
                • 1970-01-01
                • 2013-01-10
                • 2012-12-16
                • 1970-01-01
                • 1970-01-01
                • 2015-02-11
                相关资源
                最近更新 更多