递归函数可以内联吗?


134
inline int factorial(int n)
{
    if(!n) return 1;
    else return n*factorial(n-1);
}

当我阅读此书时,发现上面的代码如果编译器未正确处理,将导致“无限编译”。

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

Answers:


137

首先,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个)。


20
#pragma inline_recursion(on)。有关最大深度的文档不一致或不确定。值8、16或#pragma inline_depth的值是可能的。
peterchen

@peterchen如果内联的函数正在改变其参数之一的值,将会发生什么,我认为最好在事实内联函数而不是main。对不起,我的英语
-ob_dev

1
@obounaim:您可能会认为。MSVC没有。
SecurityMatt

23

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

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

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


7

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


6

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


3

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


2

请参阅已经给出的答案,以了解为何通常无法正常工作。

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

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

template <>
struct Factorial<0> 
{
    enum { value = 1 };
};

1
这很可爱,但是请注意,原始发布的变量参数为“ int n”。
Windows程序员

1
没错,但是当在编译时不知道n时,就没有必要进行“递归内联”了……编译器怎么能做到这一点呢?因此,就问题而言,我认为这是一个相关的选择。
yungchin

1
请参见Derek Park的示例如何进行操作:通过两次内联,您递归n >> 2次,并且从所得代码中获得2 + 2的返回值。
MSalters

1

编译器将创建一个调用图以检测并阻止此类事件。因此,它将看到该函数调用自身而不是内联。

但主要是由inline关键字和编译器开关控制(例如,即使没有关键字也可以使其自动内联小型函数。)需要注意的重要一点是,Debug编译绝不能内联,因为调用堆栈不会保留为镜像您在代码中创建的调用。


1

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

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

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

Wikipedia声明递归宏通常是非法的,但了解却很少。C和C ++可以防止递归调用,但是通过包含看起来像是递归的宏代码,翻译单元不会变得非法。在汇编程序中,递归宏通常是合法的。


0

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.