【问题标题】:Can a recursive function be inline?递归函数可以内联吗?
【发布时间】:2010-09-16 10:56:31
【问题描述】:
inline int factorial(int n)
{
    if(!n) return 1;
    else return n*factorial(n-1);
}

在阅读this时,发现上面的代码如果没有被编译器正确处理会导致“无限编译”。

编译器如何决定是否内联函数?

【问题讨论】:

    标签: c++ c compiler-construction


    【解决方案1】:

    首先,函数的inline 规范只是一个提示。编译器可以(并且经常这样做)完全忽略 inline 限定符的存在或不存在。话虽如此,编译器可以内联递归函数,就像它可以展开无限循环一样。它只需要对“展开”函数的级别设置一个限制。

    优化编译器可能会转换此代码:

    inline int factorial(int n)
    {
        if (n <= 1)
        {
            return 1;
        }
        else
        {
            return n * factorial(n - 1);
        }
    }
    
    int f(int x)
    {
        return factorial(x);
    }
    

    进入这段代码:

    int factorial(int n)
    {
        if (n <= 1)
        {
            return 1;
        }
        else
        {
            return n * factorial(n - 1);
        }
    }
    
    int f(int x)
    {
        if (x <= 1)
        {
            return 1;
        }
        else
        {
            int x2 = x - 1;
            if (x2 <= 1)
            {
                return x * 1;
            }
            else
            {
                int x3 = x2 - 1;
                if (x3 <= 1)
                {
                    return x * x2 * 1;
                }
                else
                {
                    return x * x2 * x3 * factorial(x3 - 1);
                }
            }
        }
    }
    

    在这种情况下,我们基本上已经将函数内联了 3 次。一些编译器执行此优化。我记得 MSVC++ 有一个设置来调整将在递归函数上执行的内联级别(我相信最多 20 个)。

    【讨论】:

    • 它是#pragma inline_recursion(on)。关于最大深度的文档不一致或不确定。值 8、16 或 #pragma inline_depth 的值是可能的。
    • @peterchen 如果内联函数正在改变其参数之一的值会发生什么,我认为最好将函数内联在事实而不是主函数中。对不起我的英语
    • @obounaim:你可能会这么想。 MSVC 没有。
    【解决方案2】:

    确实,如果您的编译器不能智能地运行,它可能会尝试递归地插入您的 inlined 函数的副本,从而创建无限大的代码。然而,大多数现代编译器都会认识到这一点。他们可以:

    1. 根本没有内联函数
    2. 将其内联到一定深度,如果到那时还没有终止,请使用标准函数调用约定调用函数的单独实例。这可以以高性能的方式处理许多常见情况,同时为具有较大调用深度的罕见情况留下后备。这也意味着您可以保留该函数代码的内联版本和单独版本。

    对于情况 2,许多编译器都有#pragmas,您可以设置它来指定应该执行此操作的最大深度。在 gcc 中,您还可以从命令行使用 --max-inline-insns-recursive 将其传入(查看更多信息 here)。

    【讨论】:

      【解决方案3】:

      如果可能的话,AFAIK GCC 将对递归函数进行尾调用消除。但是,您的函数不是尾递归的。

      【讨论】:

        【解决方案4】:

        编译器创建调用图;当检测到循环调用自身时,函数在一定深度后不再内联(n=1、10、100,无论编译器调整到什么)。

        【讨论】:

          【解决方案5】:

          请参阅已经给出的答案,了解为什么这通常不起作用。

          作为“脚注”,您可以使用template metaprogramming 实现您正在寻找的效果(至少对于您用作示例的阶乘)。从维基百科粘贴:

          template <int N>
          struct Factorial 
          {
              enum { value = N * Factorial<N - 1>::value };
          };
          
          template <>
          struct Factorial<0> 
          {
              enum { value = 1 };
          };
          

          【讨论】:

          • 这很可爱,但请注意原始帖子有一个可变参数“int n”。
          • 是的,但是当编译时不知道 n 时,想要“递归内联”也没有什么意义……编译器怎么能做到这一点?所以在这个问题的背景下,我认为这是一个相关的选择。
          • 参见 Derek Park 的示例如何做到这一点:通过内联两次,您递归 n>>2 次,结果代码有 2+2 个返回。
          【解决方案6】:

          一些递归函数可以转换为循环,从而有效地无限内联它们。我相信gcc可以做到这一点,但我不知道其他编译器。

          【讨论】:

            【解决方案7】:

            编译器将创建一个调用图来检测这些事情并阻止它们。所以它会看到函数调用自己而不是内联。

            但主要是由 inline 关键字和编译器开关控制(例如,即使没有关键字,你也可以让它自动内联小函数。)重要的是要注意调试编译永远不应该内联,因为调用堆栈不会保留以镜像您在代码中创建的调用。

            【讨论】:

              【解决方案8】:

              “编译器如何决定是否内联函数?”

              这取决于编译器、指定的选项、编译器的版本号、可能有多少可用内存等。

              程序的源代码仍然必须遵守内联函数的规则。无论函数是否被内联,您都必须为它可能被内联(未知次数)做好准备。

              关于递归宏通常是非法的 Wikipedia 声明看起来相当缺乏信息。 C 和 C++ 防止递归调用,但翻译单元不会因为包含看起来像递归的宏代码而变得非法。在汇编器中,递归宏通常是合法的。

              【讨论】:

                【解决方案9】:

                某些编译器(即 Borland C++)不会内联包含条件语句(if、case、while 等)的代码,因此您的示例中的递归函数不会被内联。

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 2012-04-01
                  • 2011-01-13
                  • 1970-01-01
                  • 2011-03-05
                  • 1970-01-01
                  • 2013-01-10
                  • 1970-01-01
                  • 2012-12-30
                  相关资源
                  最近更新 更多