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

news/2025/3/19 8:08:46/

编译器最优化机制通常被设计用来浓缩那些“不含函数调用”的代码,所以当你inline某个函数,或许编译器就因此有能力对它(函数本体)执行语境相关最优化。

然而inline函数也有使用的代价。

inline函数背后的整体观念是,将“对此函数的每一个调用”都以函数本体替换。因此这样做可能增加你的目标码。

在一台内存有限的机器上,过度热衷inlining会造成程序体积太大(对可用空间而言)。即使拥有虚内存,inline造成的代码膨胀亦会导致额外的换页行为(paging),降低指令高速缓存装置的击中率(instruction cache hit rate),以及伴随这些而来的效率损失。

换个角度说,若inline函数的本体很小,编译器针对“函数本体”所产出的码可能比针对“函数调用”所产出的码更小。

但记住,inline只是对编译器的一个申请,不是强制命令。这项申请可以隐喻提出,也可以明确提出。

隐喻方式是将函数定义于class定义式内:

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

这样的函数通常是成员函数,但friend函数也可被定义于class内,若真是那样,它们也被隐喻声明为inline。

明确声明inline函数的做法是在其定义式前加上关键字inline。

例如,标准的max template(来自<algorithm>)往往这样实现:

template<class T>
inline const T& std::max(const T& a, const T& b)//明确声明inline
{return a < b ? b : a;
}

上述代码中的max是个template带出了一项观察结果,我们发现inline函数和template两者通常都被定义于头文件内。这使得某些程序员认为function template一定必须是inline,但这个结论是错的。

inline函数通常一定被置于头文件内,因为大多数建置环境(build environment)在编译过程中进行inlining,而为了将一个“函数调用”替换为“被调用函数的本体”,编译器必须知道那个函数长什么样子。inlining在大多数C++程序中是编译期行为。

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

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

一个表面上看似是inline的函数是否真是inline,取决于你的建置环境,主要取决于编译器。幸运的是大多数编译器提供了一个诊断级别:若它们无法将你要求的函数inline化,会给你一个警告信息。

有时候虽然编译器有意愿inline某个函数,还是可能为该函数生成一个函数本体。

例如,若程序要去某个inline函数的地址,编译器通常必须为此函数生成一个outlined函数本体。

编译器通常不对“通过函数指针而进行的调用”实施inline,这意味着对inline函数的调用有可能被inline,也可能不被inline,取决于该调用的实施方式:

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

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

实际上构造函数和析构函数往往是inline的糟糕候选人,例如

class Base {
public://...
private:std::string bm1, bm2;//base成员1和2
};
class Derived :public Base {
public:Derived() {}//...
private:std::string dm1, dm2, dm3;//derived成员函数1-3
};

这个构造函数看起来是inline的绝佳候选人,因为它不包含任何代码。但是你的眼睛可能会欺骗你。

C++对于“对象被创建和被销毁时发生什么事”做了各种各样的保证。

当你使用new,动态创建的对象被其构造函数自动初始化;当你使用delete,对应的析构函数会被调用。当你创建一个对象,其每一个base class及每一个成员变量都会被自动构造;当你销毁一个对象,反向程序的析构行为亦会自动发生。若有个异常在对象构造期间被抛出,该对象已构造好的那一部分会被自动销毁。

在这些情况中,C++描述了什么一定会发生,但没有说如何发生。“事情如何发生”是编译器实现者的权责,但有一点很清楚,就是它们不可能凭空发生。你的程序内一定有某些代码让那些事情发生,而那些代码一定存在于某个地方。

有时就放在你的构造函数和析构函数内,所以可以想象,编译器为稍早说的那个表面上看起来为空的Derived构造函数所产生的代码,相当于以下所列:

Derived::Derived()//“空白Derived构造函数”的观念性实现
{Base::Base();//初始化“Base成分”//试图构造dm1,//若抛出异常就销毁base class成分,//并传播该异常try { dm1.std::string::string(); }catch (/*...*/) {Base::~Base();throw;}//试图构造dm2,//若抛出异常就销毁dm1、base class成分,//并传播该异常try { dm2.std::string; :string(); }catch (/*...*/) {dm1.std::string::string();Base::~Base();throw;}//试图构造dm3,//若抛出异常就销毁dm2、dm1、base class成分,//并传播该异常try { dm3.std::string::string(); }catch (/*...*/) {dm2.std::string::string();dm1.std::string::string();Base::~Base();throw;}
}

这段代码并不能代表编译器真正制造出来的代码,因为真正的编译器会以更精致复杂的做法来处理异常。尽管如此,这已能准确反应Derived的空白构造函数必须提供的行为。不论编译器在其内所做的异常处理多么精致复杂,Derived构造函数至少一定会陆续调用其成员变量和base class两者的构造函数,而那些调用(它们自身也可能被inline)会影响编译器是否对此空白函数inline。

相同理由也适用于Base构造函数,所以若它被inline,所有替换“Base构造函数调用”而插入的代码也都会被插入到“Derived构造函数调用”内(因为Derived构造函数调用了Base构造函数)。

若string构造函数恰巧也被inline,Derived构造函数将获得五份“string构造函数代码”副本,每一份副本对应于Derived对象内的五个字符串(两个来自继承,三个来自自己的声明)之一。

现在或许很清楚,“是否将Derived构造函数inline化”并非是个轻松的决定。类似思考也适用于Derived析构函数,在那儿我们必须看到“被Derived构造函数初始化的所有对象”被一一销毁,无论以哪种方式进行。

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

经过上述的讨论,对于我们有了一点提示:

一开始先不要将任何函数声明为inline,或至少将inline施行范围局限在那些“一定成为inline”或“十分平淡无奇”的函数身上。

总结

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

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


http://www.ppmy.cn/news/103649.html

相关文章

高精度电压源如何设计出来的

高精度电压源是一种用于提供高精度电压的电子设备&#xff0c;通常用于测量和控制系统。高精度电压源的设计是一个复杂的过程&#xff0c;需要考虑多个因素&#xff0c;包括电路设计、元件选型、测量误差、稳定性等。下面将从电路设计和元件选型两个方面&#xff0c;详细介绍高…

【大数据之Hive】三、Linux下安装MySQL8.0.33

1 安装MySQL &#xff08;1&#xff09;解压MySQL安装包: tar -xf mysql-8.0.33-1.el7.x86_64.rpm-bundle.tar -C /opt/module/mysql&#xff08;2&#xff09;卸载系统自带的mariadb&#xff1a; sudo rpm -qa | grep mariadb | xargs sudorpm -e --nodeps&#xff08;3&am…

自学web前端能找到工作吗?是否有必要参加前端培训?

是的&#xff0c;自学前端可以帮助您找到工作&#xff0c;参加培训是根据个人学习能力和经济实力来自己决定的。前端开发是一个相对容易入门的领域&#xff0c;并且许多人通过自学成功地找到了前端开发的工作。以下是好程序员的一些建议&#xff0c;可以帮助您在自学前端时提高…

JVM学习(十二):执行引擎

目录 一、执行引擎概述 二、执行引擎的工作过程 三、Java代码编译和执行 3.1 过程概述 3.1 javac前端编译 3.2 Java字节码的执行 3.3 编译和解释概述 3.4 高级语言理解与执行过程&#xff08;机器码、指令、汇编&#xff09; 3.4.1 机器码 3.4.2 指令 3.4.3 指…

【C++】类与对象——六个默认成员函数、构造函数的概念和特征,析构函数的概念和特征

文章目录 1.类的六个默认成员函数2.构造函数2.1构造函数的概念2.2构造函数的特性 3.析构函数3.1析构函数的概念3.2析构函数的特征 1.类的六个默认成员函数 如果一个类中什么成员都没有&#xff0c;简称为空类。   空类中真的什么都没有吗&#xff1f; 并不是&#xff0c;任何…

json和pickle模块

目录 ❤ json和pickle模块 序列化 json pickle python从小白到总裁完整教程目录:https://blog.csdn.net/weixin_67859959/article/details/129328397?spm1001.2014.3001.5502 ❤ json和pickle模块 序列化 把对象(变量)从内存中变成可存储或传输的过程称之为序列化&am…

Java并发体系-第二阶段-锁与同步-[3](仅做了解吧不好理解)

synchronized保证三大特性 synchronized保证原子性的原理 对num;增加同步代码块后&#xff0c;保证同一时间只有一个线程操作num;。就不会出现安全问题。 synchronized保证可见性的原理 synchronized保证可见性的原理&#xff0c;执行synchronized时&#xff0c;会对应lock…

热力学统计物理专题:德拜模型

简正振动的固体模型 由于固体晶格结点上相邻原子之间距离很小&#xff08;约为 米量级&#xff09;&#xff0c;故原子间存在强烈的相互作用&#xff0c;而非相互独立。在温度不高时&#xff0c;原子只在其平衡位置附近作微振动&#xff0c;且具有不同的振动频率。设系统有…