内联函数总结

2023-10-27

定义

它们看起来像函数,运作起来像函数,比宏(macro)要好得多,使用时还不需要承担函数调用的负担。当内联一个函数时,编译器可以对函数体执行特定环境下的优化工作,这样的优化对“正常”的函数调用时不可能的。

规则

inline关键字必须和函数体定义放在一起才可以实现内联,仅仅将inline放在函数声明之前不起任何作用。inline是一个用于实现的关键字而不是一个用于声明的关键字。对于类方法,定义在类内部的方法自动成为内联函数。

实现思想

内联函数的基本思想是将每个函数调用以它的代码体来替换,很可能会增加整个目标代码的体积,过分使用内联产生的程序会因为没有太大的体积导致可用空间不够。即使可以使用虚拟内存,内联所造成的代码膨胀也可能会导致不合理的页面调度行为(系统颠簸),这将使你的程序运行的很慢,过多的内联还会降低指令高速缓存的命中率,从而使取指令的速度降低,因为从主存取指令当然比缓存要慢。另一方面,如果内联函数体非常短,编译器为这个函数体生成的代码就会真的比为函数调用生成的代码要小的多,如果是这种情况,内联这个函数将会确实带来更小的目标代码和更高的缓存命中率。

inline指令就像register,它只是对编译器的一种提示,而不是命令。也就是说,只要编译器愿意,他就可以随意的忽略掉你的指令,事实上编译器常常会这么做。例如,大多数编译器会拒绝内联“复杂”的函数(例如包含循环和递归的函数);还有,即使是最简单的虚函数调用,编译器的内联处理程序对它也爱莫能助(这一点也不奇怪,virtual的意思是“等到运行时再决定调用那个函数”,inline的意思是“在编译期间将调用之处用被调函数来替代”,如果编译器甚至不知道哪个函数将被调用,当然就不能责怪它拒绝生成内联调用了)。

问题及对应的解决规则

假设写了某个函数f并声明为inline,如果出于什么原因,编译器决定不对它内联,那将会发生些什么呢?最明显的一个回答是将f作为一个非内联函数来处理:为f生成代码时就象它是一个普通的”外联”函数一样, 对f的调用也象对普通函数调用那样进行。

理论上来说确实应该这样发生,但理论和现实往往会偏离,现在就属于这种情况。因为,这个方案对解决”被外联的内联”(outlined inline)这一问题确实非常理想,但它加入到C++标准中的时间相对较晚。较早的C++规范告诉编译器制造商去实现的是另外不同的行为,而且这一旧的行为在现在的编译器中还很普遍,所以必须理解它是怎么一
回事。

以上可以归结为:一个给定的内联函数是否真的被内联取决于所用的编译器的具体实现。幸运的是,大多数编译器都可以设置诊断级,当声明为内联的函数实际上并没有被内联时,编译器就会为你发出警告信息。

内联函数的定义实际上都是放在头文件中,这使得多个要编译的单元(源文件)可以包含同一个头文件,共享头文件内定义的内联函数所带来的益处。

//文件example.h
inline void f(){...} //f的定义
...
//文件source1.cpp
#include"example.h"  //包含f的定义
...                  //包含对f的调用
//文件source2.cpp
#include"example.h"  //包含f的定义
...                  //调用f

问题:
假设现在采用旧的”被外联的内联”规则,而且假设f没有被内联,那么,当source1.cpp被编译时,生成的目标文件中将包含一个称为f的函数,就象f 没有被声明为inline一样。
同样地,当source2.cpp被编译时,产生的目标文件也将包含一个称为f的函数。当想把两个目标文件链接在一起时,编译器会因为程序中有两个f的定义而报错。在两个cpp文件编译生成的.obj文件中都存在”被外联的内联”方法f。

为了防止这一问题:

旧规则规定:

对于未被内联的内联函数,编译器把它当成被声明为static 那样处理,即,使它局限于当前被编译的文件。
具体到刚才看到的例子中,遵循旧规则的编译器处理source1.cpp中的f时,就象f在 source1.cpp中是静态的一样;处理source2.cpp中的f时,也把它当成在source2.cpp中是静态的一样。
这一策略消除了链接时的错误,但带来了开销:每个包含 f 的定义(以及调用 f )的被编译单元都包含自己的 f 的静态拷贝。
如果 f 自身定义了局部静态变量,那么每个 f 的拷贝都有此局部变量的一份拷贝,这必将会让程序员大吃一惊,因为一般来说,函数中的“static”意味着“只有一份拷贝”。

新规则

将 f 作为一个非内联函数来处理:为 f 生成代码时就像它是一个普通的“外联”函数一样,对 f 的调用也像对普通函数调用那样进行。

无论新规则还是旧规则,如果内联函数没被内联,每个调用内联函数的地方还是得承担函数调用的开销;如果是旧规则,还得承受代码体积的增加,因为每个包含(或调用)f 的被编译单元都有一份 f 的代码及其静态变量的拷贝!(更糟糕的是,每个 f 的拷贝以及每个 f 的静态变量往往处于不同的虚拟内存页面,所以两个对 f 的不同拷贝进行调用可能导致多个页面错误)。

更多问题

有时编译器即使很想内联一个函数,却不得不为这个内联函数生成一个函数体。特别是,如果程序中要取一个内联函数的地址,编译器就必须为此生成一个函数体。编译器怎么能产生一个指向不存在的函数的指针呢?

inline void f(){...}     //同上
void (*pf)() = f;
int main(){
    f();       //对f的内联调用
    pf();      //通过pf对f的非内联调用
}

这种情况很荒谬:f 的调用被内联了。
旧的规则:
每个取 f 地址的被编译单元还是各自生成了次函数的静态拷贝。

新规则下:
不管涉及的被编译单元有多少,将只生成唯一一个 f 的外部拷贝。

即使你从来不使用函数指针,这类”没被内联的内联函数”也会找上你的门,因为不只是程序员会使用函数指针,有时编译器也这么做。特别是,编译器有时会生成构造函数和析构函数的外部拷贝,这样就可以通过得到那些函数的指针,方便地构造和析构类的对象数组。

实际上,随便一个测试就可以证明构造函数和析构函数常常不适合内联;甚至,情况比测试结果还糟。例如,看下面这个类Derived的构造函数:

class Base{
public:
    ...
private:
    string bm1, bm2;
};

class Derived: public Base{
public:
    Derived(){}    //Derived的构造函数是空的,但,真的是空的吗?
private:
    string dm1, dm2, dm3;
}

这个构造函数看起来的确象个内联的好材料,因为它没有代码。但外表常常欺骗人!仅仅因为它没有代码?实际上,它含有相当多的代码。

C++ 就对象创建和销毁时发生的事件有多方面的规定。当使用new时,动态创建的对象将自动地被它们的构造函数初始化,当使用 delete时析构函数怎样被调用。当创建一个对象时,对象的每个基类以及对象的每个数据成员会被自动地创建;当对象被销毁时,会自动地执行相反的过程(即析构)。

C++规定了哪些必须发生,但没规定”怎么”发生。”怎么发生”取决于编译器的实现者,但要弄清楚的是,这些事件不是凭空自己发生的。程序中必然有什么代码使得它们发生,特别是那些由编译器的实现者写的、在编译其间插入到你的程序中的代码,必然也藏身于某个地方。有时,它们就藏身于你的构造函数和析构函数。

所以,对于上面那个号称为空的Derived的构造函数,有些编译器会为它产生相当于下面的代码:

//一个Derived构造函数的可能实现
Derived::Derived(){
    //如果在堆上创建对象,为其分配堆内存
    if(本对象在堆上)
        this = ::operator new(sizeof(Derived));
    Base::Base();    //初始化Base部分
    dm1.string();    //构造dm1
    dm2.string();    //构造dm2
    dm3.string();    //构造dm3
}

调用operator new(如果需要的话)的代码、构造基类部分的代码、构造数据成员的代码都会神不知鬼不觉地添加到你的构造函数中,从而增加构造函数的体积,使得构造函数不再适合内联。当然,同样的分析也适用于Base的构造函数,如果Base的构造函数被内联,添加到它里面的所有代码也会被添加到Derived的构造函数(Derived的构造函数会调用Base的构造函数)。如果string的构造函数恰巧也被内联,Derived的构造函数将得到其代码的5个拷贝,每个拷贝对应于Derived对象中5个string中的一个(2个继承而来,3个自己声明)。现在你应该明白,内联Derived的构造函数并非可以很简单就决定的!当然,类似的情况也适用于Derived的析构函数,无论如何都要清楚这一点:被Derived的构造函数初始化的所有对象都要被完全销毁。刚被销毁的对象以前可能占用了动态分配的内存,那么这些内存还需要释放。

内联函数的使用规则

程序库的设计者必须预先估计到声明内联函数带来的负面影响:想对程序库中的内联函数进行二进制代码升级是不可能的。换句话说,如果 f 是库中的一个内联函数,用户会将 f 的函数体编译到自己的程序中。如果程序库的设计者后来要修改 f ,所有使用f 的用户程序必须重新编译。相反,如果 f 是非内联函数,对 f 的修改仅需要用户重新链接,这就比需要重新编译大大减轻了负担;如果包含这个函数的程序库是被动态链接的,程序库的修改对用户来说完全是透明的。

内联函数中的静态对象常常表现出违反直觉的行为,如果函数中包含静态对象,通常要避免将它声明为内联函数。

一般来说,实际编程时最初的原则是不要内联任何函数,除非函数确实很小很简单,象下面这个age函数:

class Person{
public:
    int age() const {return personAge; }
    ...
private:
    int personAge;
    ...
};

慎重地使用内联,不但给了调试器更多发挥作用的机会,还将内联的作用定位到了正确的位置:它是一个根据需要而使用的优化工具。不要忘了从无数经验得到的,一个程序往往花80%的时间来执行程序中20%的代码。这是一条很重要的定律,因为它提醒你,作为程序员的一个很重要的目标,就是找出这20%能够真正提高整个程序性能的代码。你可以选择内联你的函数,或者没必要就不内联,但这些选择只有作用在”正确”的函数上才有意义。 一旦找出了程序中那些重要的函数,以及那些内联后可以确实提高程序性能的函数(这些函数本身依赖于所在系统的体系结构),就要毫不犹豫地声明为inline。同时,要注意代码膨胀带来的问题,看看是否有内联函数没有被编译器内联。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

内联函数总结 的相关文章

  • Play With Docker

    文章目录 Play with Docker PWD 1 Getting Started The command you just ran What is a container What is a container image 2 Our
  • Windows 和 Linux 上安装 TTF 字体的方法

    前言 在之前的文章中 我们是通过引入字体文件的方法解决了平台字体兼容性的问题 有同学提出了一个问题 为什么要通文件引入的方式解决问题 难道不考虑带宽问题吗 其实 我们一开始考虑的方案是将字体文件安装在运行项目的每台客户机上 但是 因为使用项

随机推荐

  • 第四天 Java 数组与排序

    数组与排序 一 数组 1 数组的常见概念 二 一维数组 1 1 一维数组的声明方式 1 2 一维数组的初始化 1 动态初始化 2 静态初始化 1 3 一维数组内存解析 1 4 随机数的产生 三 多维数组的使用 1 二维数组 数组中的数组 2
  • 基于模型预测控制(MPC)的悬架系统仿真分析

    目录 前言 1 悬架系统 2 基于MPC的悬架系统仿真分析 2 1 simulink模型 2 2仿真结果
  • 1.深度学习入门:感知机是什么和感知机的实现(详细讲解)

    1 深度学习入门 感知机是什么和感知机的实现 感知机 感知机是什么 简单逻辑电路 感知机的实现 简单的实现 导入权重和偏置 使用权重和偏置的实现 感知机 感知机是什么 感知机 Perceptron 是一种二元线性分类模型 由美国学者Fran
  • ovirt 安装

    第一步 通过oVirt node iso安装host 从本站 安装包下载 页面或其它渠道获取到oVirt4 4版本的node iso 本次部署使用的是ovirt node ng installer 4 4 1 2020080418 el8
  • vim编辑器使用教程

    文章目录 前言 一 vim 的三种工作模式 二 vim 基本操作 1 编辑 2 复制粘贴 3 撤销 4 跳转 5 查找和替换 6 自动缩进 7 分屏 8 其他 三 vim 配置文件 前言 vim 是 Linux 系统内置的 文本编辑器 用于
  • docker系列:1、docker概述和学习资料

    docker系列 1 docker概述和学习资料 文章目录 docker系列 1 docker概述和学习资料 1 前言 2 docker概述和学习资料 3 最后 1 前言 之前总结完了GoFrame框架 我接下来是想将我的个人网站做下升级
  • MapReduce工作流程

    1 MapReduce工作流程图 2 流程详解 上面的流程是整个MapReduce最全工作流程 但是Shuffle过程只是从第7步开始到第16步结束 具体Shuffle过程详解 如下 1 MapTask收集我们的map 方法输出的kv对 放
  • ucint核心边缘分析_【5G研报】边缘计算及重点个股

    这是一篇清明前发过的文章 由于微信后台两天不回复网友留言则无法私信各位 故重新发布 对5G感兴趣的朋友老规矩 点击关注 后台留言或发消息 边缘计算 搞研究 选个股 我们是专业的 一篇关于边缘计算的万字研报 感兴趣的点个关注 更多资讯尽在神光
  • 2651. 计算列车到站时间

    文章目录 Tag 题目来源 题目解读 解题思路 方法一 数学 知识回忆 除法运算 写在最后 Tag 数学 题目来源 2651 计算列车到站时间 题目解读 给你一个列车预计到达时间点和一个列车延误的时间 请返回列车实际的到达时间 解题思路 方
  • springboot和springcloud的联系与区别

    什么是springboot Spring Boot是一个用于简化Spring应用程序开发的框架 它提供了一种约定优于配置的方式 通过自动配置和快速开发能力 可以快速搭建独立运行 生产级别的Spring应用程序 在传统的Spring应用程序开
  • 程序员应该避开的20个低级不良用户体验

    前往老猿Python博文目录 https blog csdn net LaoYuanPython 前2天碰到一件事 只因职业信息没登记汉口银行的系统居然禁止我使用银行卡账号 这样的神操作一看十有八九是程序员干的 由此联想到平时开发中遇到的一
  • Core Java(十三)

    Java API5 0新特性 优化的功能 对编译器进行的优化 静态引入 什么是静态引入 将类的静态成员预先引用进来 直接使用就可以了 例如 System out 直接写成out printl 就可以了 语法 import static 包名
  • H5页面判断来源是 微信小程序、百度小程序、微信浏览器、其他环境

    小程序打开H5页面有一些链接需要隐藏掉 不让用户随意跳转 在页面中引入微信 第一个 百度小程序 第二个 外部 js 在页面的js中判断即可
  • vlan接口类型和划分

    vlan接口类型 access 一般不带标签发送 负责标签压入与剥离 trunk 一般带标签发送 每个trunk接口都有一个pvid hybrid 发的时候可以带多个pvid 控制数据带标签或不带标签发送 access端口收到没有没有带ta
  • .NET MAUI 多平台应用 UI 应用

    NET 多平台应用 UI 使用文档 NET 多平台应用 UI NET MAUI 允许你使用面向 Android iOS macOS Windows 和 Tizen 上的移动和桌面外形规格的 NET 跨平台 UI 工具包生成本机应用 本教程介
  • chrome扩展开发中文教程

    chrome扩展开发中文教程 文档地址 http chrome cenchy com index html 既然是中文文档 可能存在翻译的过程 就会出现翻译更新不及时的情况 如果看最新最准确的文档 请移步到官方文档
  • 轻松高薪之---java基础(一)

    轻松高薪系列将分为五期为大家呈现 java基础 WEB 数据库 框架 技术点五大部分 由于每一个内容比较多 每一部分将分为若干小结进行分享 欢迎持续跟踪 一 Java 基础 1 Java 基础 知识 1 1 面向对象的特征 了解 面向对象的
  • CUDA编程(七)共享内存与Thread的同步

    CUDA编程 七 共享内存与Thread的同步 在之前我们通过block 继续增大了线程的数量 结果还是比较令人满意的 但是也产生了一个新的问题 即 我们在CPU端的加和压力变得很大 所以我们想到能不能从GPU上直接完成这个工作 我们知道每
  • ES6笔记( 五 )- Object

    目录 新增的对象字面量语法 成员速写 方法速写 计算属性名 新增的Object方法 Object is Object assign Object setPrototypeOf Object keys Object values Object
  • 内联函数总结

    定义 它们看起来像函数 运作起来像函数 比宏 macro 要好得多 使用时还不需要承担函数调用的负担 当内联一个函数时 编译器可以对函数体执行特定环境下的优化工作 这样的优化对 正常 的函数调用时不可能的 规则 inline关键字必须和函数