C++ 内联函数 摘自 C++ 应用程序性能优化

内联函数

在C++语言的设计中,内联函数的引入可以说完全是为了性能的考虑。因此在编写对性能要求比较高的C++程序时,非常有必要仔细考量内联函数的使用。 所谓"内联",即将被调用函数的函数体代码直接地整个插入到该函数被调用处,而不是通过call语句进行。当然,编译器在真正进行"内联"时,因为考虑到被内联函数的传入参数、自己的局部变量,以及返回值的因素,不仅仅只是进行简单的代码拷贝,还需要做很多细致的工作,但大致思路如此。
开发人员可以有两种方式告诉编译器需要内联哪些类成员函数,一种是在类的定义体外;一种是在类的定义体内。
(1)当在类的定义体外时,需要在该成员函数的定义前面加"inline"关键字,显式地告诉编译器该函数在调用时需要"内联"处理,如:

class Student
{
public:
        String  GetName();
        int     GetAge();
        void        SetAge(int ag);
        ……
private:
        String  name;
        int     age;
        ……
};
 
inline String GetName()
{
        return name;
}
 
inline int GetAge()
{
        return age;
}
 
inline void SetAge(int ag)
{
        age = ag;
}

(2)当在类的定义体内且声明该成员函数时,同时提供该成员函数的实现体。此时,"inline"关键字并不是必需的,如:

class Student
{
public:
        String  GetName()       { return name; }
        int     GetAge()        { return age; }
        void        SetAge(int ag)  { age = ag; }
        ……
private:
        String  name;
        int     age;
        ……
};

当普通函数(非类成员函数)需要被内联时,则只需要在函数的定义时前面加上"inline"关键字,如:

    inline int DoSomeMagic(int a, int b)
{
        return a * 13 + b % 4 + 3;
}

因为C++是以"编译单元"为单位编译的,而一个编译单元往往大致等于一个".cpp"文件。在实际编译前,预处理器会将"#include"的各头文件的内容(可能会有递归头文件展开)完整地拷贝到cpp文件对应位置处(另外还会进行宏展开等操作)。预处理器处理后,编译真正开始。一旦C++编译器开始编译,它不会意识到其他cpp文件的存在。因此并不会参考其他cpp文件的内容信息。联想到内联的工作是由编译器完成的,且内联的意思是将被调用内联函数的函数体代码直接代替对该内联函数的调用。这也就意味着,在编译某个编译单元时,如果该编译单元会调用到某个内联函数,那么该内联函数的函数定义(即函数体)必须也包含在该编译单元内。因为编译器使用内联函数体代码替代内联函数调用时,必须知道该内联函数的函数体代码,而且不能通过参考其他编译单元信息来获得这一信息。

如果有多个编译单元会调用到某同一个内联函数,C++规范要求在这多个编译单元中该内联函数的定义必须是完全一致的,这就是"ODR"(one-definition rule)原则。考虑到代码的可维护性,最好将内联函数的定义放在一个头文件中,用到该内联函数的各个编译单元只需#include该头文件即可。进一步考虑,如果该内联函数是一个类的成员函数,这个头文件正好可以是该成员函数所属类的声明所在的头文件。这样看来,类成员内联函数的两种声明可以看成是几乎一样的,虽然一个是在类外,一个在类内。但是两个都在同一个头文件中,编译器都能在#include该头文件后直接取得内联函数的函数体代码。讨论完如何声明一个内联函数,来查看编译器如何内联的。继续上面的例子,假设有个foo函数:

#include "student.h"
...
 
void foo()
{
        ...
        Student abc;
        abc.SetAge(12);
        cout << abc.GetAge();
        ...
}

foo函数进入foo函数时,从其栈帧中开辟了放置abc对象的空间。进入函数体后,首先对该处空间执行Student的默认构造函数构造abc对象。然后将常数12压栈,调用abc的SetAge函数(开辟SetAge函数自己的栈帧,返回时回退销毁此栈帧)。紧跟着执行abc的GetAge函数,并将返回值压栈。最后调用cout的<<操作符操作压栈的结果,即输出。

#include "student.h"
...
 
void foo()
{
        ...
        Student abc;
        {
            abc.age = 12;
        }
        int tmp = abc.age;
        cout << tmp;
        ...
}

这时,函数调用时的参数压栈、栈帧开辟与销毁等操作不再需要,而且在结合这些代码后,编译器能进一步优化为如下结果:

#include "student.h"
...
 
void foo()
{
        ...
        cout << 12;
        ...
}

这显然是最好的优化结果;相反,考虑原始版本。如果SetAge/GetAge没有被内联,因为非内联函数一般不会在头文件中定义,这两个函数可能在这个编译单元之外的其他编译单元中定义。即foo函数所在编译单元看不到SetAge/GetAge,不知道函数体代码信息,那么编译器传入12给SetAge,然后用GetAge输出。在这一过程中,编译器不能确信最后GetAge的输出。因为编译这个编译单元时,不知道这两个函数的函数体代码,因而也就不能做出最终版本的优化。

从上述分析中,可以看到使用内联函数至少有如下两个优点。

(1)减少因为函数调用引起开销,主要是参数压栈、栈帧开辟与回收,以及寄存器保存与恢复等。

(2)内联后编译器在处理调用内联函数的函数(如上例中的foo()函数)时,因为可供分析的代码更多,因此它能做的优化更深入彻底。前一条优点对于开发人员来说往往更显而易见一些,但往往这条优点对最终代码的优化可能贡献更大。

原文地址:https://www.cnblogs.com/gardenofhu/p/10063460.html