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

目录

1.前言

2.实例分析

3.总结


1.前言

Inline函数,多棒的点子。它们看起来像函数,动作像函数,比宏好的多,可以调用它们又不需蒙受函数调用所导致的额外开销。

其实实际的好处比想到的还要多,因为“免除函数调用成本”只是故事的一部分而已。编译器最优化机制通常被设计用来浓缩那些“不含函数调用”的代码,所以当你inline某个函数,或许编译器就因此有能力对函数本体执行语境相关最优化。

然而编写程序就像现实生活一样,没有白吃的午餐。Inline函数也不例外。Inline函数背后的整体观念是将对此函数的每一个调用都以函数本体替换之,这样可能会增加目标码(object code)大小。在一台内存有限的机器上,过度热衷于inlining会导致程序体积太大。即使拥有需内存,Inline造成的代码膨胀也会导致额外的换页行为,降低指令高速缓冲装置的击中率,以及伴随这些而来的效率损失。

换个角度说,如果inline函数的本体很小,编译器针对函数本体所产出的码可能比针对函数调用所产出的码更小。果真如此,将函数inlining确实可能导致较小的目标码和较高的指令高速缓存装置集中率。

2.实例分析

记住,inline只是对编译器的一个申请,不是强制命令,这项申请可以隐喻提出,也可以明确提出。隐喻方式是将函数定义于class定义式内

class Person{

    public:
        ....
        int age() const {return theAge;}//一个隐喻的inline申请,age被定义于class定义式内
        ...
    private:
        int theAge;
};

这样的函数通常是成员函数,但条款46说friend函数也可被定义于class内,如果真是这样,它们也是被隐喻声明为inline。

明确声明inline函数的做法则是在其定义式前加上关键字inline,例如标准的max template往往是这样实现出来的:

template<typename T>//明确申请inline
inline const T& std::max(const T& a,const T& b){
    return a<b?b:a;//std::max之前有关键字inline
}

max是个template,带出来一项观察结果:我们发现inline函数和templates两者通常都被定义于头文件中,这使得某些程序员以为function template一定必须是inline。这个结论不但无效,还可能有害。

inline函数通常被置于头文件内,因为大多数建置环境在编译过程中进行了inlining,而为了将一个”函数调用“替换为”被调用函数的本体“,编译器必须知道那个函数是什么样的。某些建置环境可以在链接期完成inlining,少量建置环境如基于.NET CLI的托管环境竟可以在运行期完成inlining。然而这样的环境是例外,不是通例。inlining在大多数C++程序中是编译器行为。

Template通常也被置于头文件内,因为它一旦被使用,编译器为了将她具体化,需要知道它长什么样子。

Template的具现化与inlining无关。如果你正在写一个template而你认为所有根据template具现出来的函数都应该inlined,请将此template声明为inline;这就是上述std::max代码的作为。但如果你写的template没有理由要求它所具现的每一个函数都是inlined,就应该避免将这个template声明为inline,inlining需要成本。

大部分编译器拒绝将太过复杂的函数inlining,而所有对virtual函数的调用也都会使得inlining落空,因为virtual意味着等待,直到运行期才确定调用哪个函数,而inline意味着执行前,先将调用动作替换为被调用函数的本体。

总而言之,上述叙述整合的意思就是:一个表面看似inline的函数是否真是inline,取决于你的建置环境,主要取决于编译器。幸运的是大多数编译器提供了一个诊断级别:如果无法将你要求的函数inline化,会给你个警告信息。

有时候虽然编译器有意愿inlining某个函数,还是可能为该函数生成一个函数本体。举个例子,如果程序要取某个inline函数的地址,编译器通常必须为此函数生成一个outlined函数本体。并且编译器通常不对“通过函数指针而进行的调用”实施inlining,这意味着对inline函数调用有可能被inlined,也可能不被inlined,取决于对该函数的实施方式:

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

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

实际上构造函数和析构函数往往是inlining的糟糕候选人,比如考虑以下derived class构造函数:

class Base{
    public:
    ...
    private:
        std::string bm1,bm2;//baset成员1和2
};
class Derived:public Base{

    public:
        Derived(){}
        ,,,
    private:
        std::string d,1,dm2.dm3;derived成员1,2,3
};
这个构造函数看上去是inlining的绝佳候选人,因为它根本不含任何代码。但是实际上不是这样的。

C++对于“对象被创建和被销毁时发生什么事”做了各种保证。当你使用new,动态创建的对象被其构造函数自动化;当你使用delete,对应的析构函数会被调用。 当你创建一个对象,其每一个base class及每一个成员变量都会被自动构造;当你销毁一个对象时,反向程序的析构行为也会自动发生。如果有个异常在对象构造期间被抛出,该对象已构造好的那一部分会被自动销毁。在这些情况中c++描述什么一定发生了,但没有说如何发生。“事情如何发生”是编译器的责任,不过有一点很清楚,那就是它们不会凭空发生。所以我们可以想像,编译器为稍早说的那个表面上看起来空的Derived构造函数所产生的代码,相当于以下所列:

Derived::Derived()
{
    Base::Base();//初始化“Base成分”
    try{dm1.std::string::string();}试图构造dm1
    catch(...){//如果抛出异常
        Base::~Base();//销毁base class成分
        throw;
    }
    try{dm2.std::string::string();}//试图构造dm2
    catch(...){//如果抛出异常
        dm1.std::string::string();//销毁dm1
        Base::~Base();//销毁base class成分
        throw;
    }
    try{dm3.std::string::string();}//试图构造dm2
    catch(...){//如果抛出异常
        dm2.std::string::string();//销毁dm2
        dm1.std::string::string();//销毁dm1
        Base::~Base();//销毁base class成分
        throw;
    }
}

这段代码并不能代表编译器真正制造出来的代码,因为真正的编译器会以更精致的做法来处理异常,尽管如此,这也能大致反映derived的空白构造函数必须提供的行为。

程序设计者必须评估“将函数声明为inline”的冲击:inline函数无法随着程序库的升级而升级。换句话说如果f是程序库的一个inline函数,客户将“f函数本体”编进其程序中,一旦程序设计者决定改变f所有用到的f的客户端程序都必须重新编译。然而如果f是Non-inline函数,一旦它有任何修改,客户端只需重新连接就好,远比重新编译的负担少很对。如果程序采取动态连接,升级版函数甚至可以不知不觉地被应用程序吸纳。

从实用的观点出发,有一个事实比其它因素更重要:大部分调试器面对inline函数都束手无策.

3.总结

将以上内容总结为以下两点:

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

2.不要只因为function template出现在头文件,就将它们声明为inline.


 

  • 18
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值