Effective C++条款30:透彻了解inlining的里里外外(Understand the ins and outs of inlining)


《Effective C++》是一本轻薄短小的高密度的“专家经验积累”。本系列就是对Effective C++进行通读:

第5章:实现

在这里插入图片描述


条款30:透彻了解inlining的里里外外

1、inline函数的优缺点

优点

  • 避免函数调用的开销

  实际上比你想象的要获取的更多,因为避免函数调用的开销只是这个故事的一部分。编译器最优化是为了浓缩没有函数调用的代码而设计,所以当你inline一个函数时,你可能使编译器在函数体上执行特定场景下的优化操作。大多数编译器不会在outlined的函数调用上执行这样的优化。

缺点

  • 以函数体代替函数调用,因此目标码增大。

  • 在内存有限的机器上,过度的inlining会造成占用空间过大的问题

  • 即使拥有虚内存,inline造成的代码膨胀也会造成额外的换页行为,降低指令高速缓存装置的集中率,以及伴随效率的损失。

2、隐式内联和显式内联

  需要注意的是inline是对编译器的请求而不是强制命令。请求可以显示或者隐式的提出来。

2.1 隐式内联

  隐式内联:当成员函数定义在类的内部时,这个函数是隐式inline的(隐式内联只有这一种情况)。例如:

class Person {
    
    
public:
    //隐式内联(编译器自动申请),这个函数(age)不仅在类中声明,还在类中进行了定义
    int age()const {
    
     return theAge; }
private:
    int theAge;
};

  这样的函数通常是成员函数,但是条款46中解释道friend函数也能在类中定义。如果是这样,它们也会被隐式声明成inline。

2.2 显式内联

  显式内联:我们也可以通过inline关键字显式的指出一个函数作为内联函数。例如:

template<typename T>
inline const T& std::max(const T& a, const T& b)
{
    
    
    return a < b ? b : a;
}

3、函数模板必须inline么?

  max是个template可以让让我们联想到inline函数和模板通常被定义在头文件中的。因此一些程序要就下结论函数模板就必须是inline的。这个结论既无效并可能会有潜在的危害,值得我们对此进行分析。

  inline函数通常被置于头文件内,因为大多数建置环境在编译过程中进行inlining,编译器必须了解这个函数长成什么样子。某些编译环境能够在链接的时候执行内联,甚至有一些能够在运行时进行内联(如基于.NET CLI的托管环境),这样的环境都是例外,但不是通用规则。在大多数C++程序中inline是编译时活动。

  template模板通常也被置于头文件内,因此它一旦被使用,编译器为了将其实例化,也需要知道它长什么样子。

template的具现化与inlining无关:

  • 如果你写的模板认为具体实现处的函数应该是inlining的,那么就将template声明为inline

  • 如果你写的代码没有理由应该是inlining的,那么就将不要将template声明为inline(因为可能会产生代码膨胀)

4、编译器忽略内联的情况

  即使你将函数声明为inline的,inline也只是一个对编译器的请求,而编译器可能会将其忽略。例如:

  • 太过复杂的函数:例如带有循环或递归

  • 对virtual函数的调用:virtual意味着“只有在运行时才能决定调用哪个函数,”而inline意味着“执行程序之前,在调用点处用函数体进行替换”。如果编译器不知道将会调用哪个函数,你就不能因为拒绝为函数体内联而责备它。

这些叙述整合起来的意思就是:

  • 一个表面看似inline的函数,或者显式使用inline声明的函数,到底是不是一个内联函数,取决于你所使用的编译环境——而这个编译环境主要是只编译器。幸运的是,编译器会对这个过程进行诊断,如果inline一个函数失败了,它会发出一个警告(见条款53)。

  有时候即使编译器迫切的希望对函数进行inline,它们也会为其生成一个单独的函数体。例如,如果你的程序需要获知内联函数的地址,编译器就必须为其生成一个outline的函数体。它们不能使用一个不存在的函数指针吧?加上如下事实:编译器使用函数指针进行函数调用时不会为其进行inline,这意味着对内联函数的调用可能会被内联也可能不会,取决于函数调用是如何进行的:

inline void f() {
    
    ...}  //假设编译器有意愿inline对f的调用 
void (*pf )() = f;  //pf指向f
...
f();   //这个调用将被inlined,因为它是一个正常的调用
pf();   //这个调用或许不被inlined,因为它通过函数指针调用

  未被inline的inline函数还是会缠住你,即使你从未使用函数指针也是如此,因为并不是只有程序员才会需要函数指针。有时候编译器也会为构造函数和析构函数生成一份outline副本,这样一来它们就可以获得指针指向那些函数,在array内部元素的构造和析构过程中使用。

5、构造函数和析构函数是否要被inline?

  构造函数和析构函数通常情况下是inline函数的槽糕候选人,而不像表面看上去那样,考虑以下Derived class的构造函数:

class Base{
    
    
public:
    //...
private:
    std::string bm1, bm2;
};
 
class Derived :public Base {
    
    
public:
    Derived() {
    
    }   //构造函数为空
private:
    std::string dm1, dm2, dm3;
};

  上面的Derived构造函数为空,此时你可能会认为Derived的构造函数时inlining的,但是实际上不是这样的。

  我们知道当对象被创建或者析构的时候C++必须保证一些事情的发生:

  • 当你使用new的时候,你的动态创建的对象由它们的构造函数自动初始化;当你使用delete时,对应的析构函数要被触发。

  • 当你创建一个对象时,对象的基类部分和它的每个数据成员都会被自动构建,当对象被销毁的时候相反的过程也就是自动析构就会发生。

  • 如果在构造或者析构的时候抛出异常,已经被构建出来的对象的任何部分都应该被自动释放。

  上面的Derived的构造函数虽然为空,但是其有3个数据成员,基类有2个数据成员。下面是伪代码,编译器会自动为这些数据成员进行初始化:

//伪代码
Derived::Derived() 
{
    
    
    //下面是编译器为空的Derived构造函数添加的代码
    Base::Base(); //初始化BaSE部分
 
    try {
    
    
        dm1.std::string::string();
    }
    catch (...) {
    
    
        Base::~Base();
        throw;
    }
 
    try {
    
    
        dm2.std::string::string();
    }
    catch (...) {
    
    
        dm1.std::string::~string();
        Base::~Base();
        throw;
    }
 
    try {
    
    
        dm3.std::string::string();
    }
    catch (...) {
    
    
        dm2.std::string::~string();
        dm1.std::string::~string();
        Base::~Base();
        throw;
    }
}

  这么写并不代表着编译器一定会这么做,因为编译器处理异常的方式更加复杂。但是这精确的反映出Derived的空构造函数必须提供什么。不管编译器对异常处理的实现多么复杂,Derived的构造函数必须为其数据成员和基类调用构造函数,这些调用(可能它们本身是inline的)会影响inline的吸引力。

  同样的原因适用于基类构造函数,因此如果它被inline了,它里面的代码同样会被插入到Derived构造函数中(Derived构造函数会调用基类构造函数。)。并且如果string构造函数恰恰也被inline了,Derived构造函数会增加5份函数代码的拷贝(对应Derived中的5个string),现在你应该明白了为什么对Derived构造函数进行inline是一个没脑子的决定。同样的考虑也适用于Derived析构函数,我们必须看到被Derived构造函数初始化的对象被合适的销毁掉。

6、牢记

  • 将大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。

  • 不要只因为function templates出现在头文件,就将它们声明为inline。

总结

期待大家和我交流,留言或者私信,一起学习,一起进步!

猜你喜欢

转载自blog.csdn.net/CltCj/article/details/128259860
ins