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

在阅读本文时,发现如果编译器未正确处理上述代码,将导致“无限编译”。

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

4

9 回答 9

151

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

于 2008-10-10T05:47:13.117 回答
25

事实上,如果你的编译器没有智能地行动,它可能会尝试inline递归地插入你的 d 函数的副本,从而创建无限大的代码。然而,大多数现代编译器都会认识到这一点。他们可以:

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

对于情况 2,许多编译器都有#pragmas 您可以设置以指定应该执行此操作的最大深度。在gcc中,您也可以从命令行传入它(请参阅此处--max-inline-insns-recursive的更多信息)。

于 2008-10-10T05:42:29.803 回答
8

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

于 2008-10-10T05:56:47.793 回答
6

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

于 2008-10-10T05:45:12.527 回答
3

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

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

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

template <>
struct Factorial<0> 
{
    enum { value = 1 };
};
于 2008-10-10T05:50:24.920 回答
3

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

于 2008-10-16T19:53:55.420 回答
1

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

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

于 2008-10-10T05:36:45.633 回答
1

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

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

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

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

于 2008-10-10T05:50:15.230 回答
0

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

于 2008-10-17T05:08:58.573 回答