【函数调用需要哪些开销,内联函数又做了什么?】

系列文章目录

        欢迎大家订阅我的《计算机底层原理》、《自顶向下看Java》专栏,我会持续为大家输出优质内容,能够帮助到各位就是对我最大的鼓励!


目录

系列文章目录

前言

一、函数调用需要哪些开销

1.压栈于弹栈开销:

2.寄存器保存于恢复开销:

3.参数传递开销

4.栈帧生成开销

5.寻址开销

6.代码段切换开销

7.上下文切换的开销

8.内存访问开销

9.返回值传递开销

10.函数调用的指令开销

二、内联函数省略掉了哪些开销

1.函数调用开销

2.参数传递开销

4.代码段切换开销

5.指令开销

6.返回值传递开销

7.函数调用指令的开销

8.内存访问开销

 三、内联函数到底做了什么 

总结


一、函数调用需要哪些开销

1.压栈于弹栈开销:

        当函数被调用的时候、参数局部变量、返回地址等信息全部需要被压入调用栈,也就是我们平常所说的函数栈帧,函数执行完毕以后这些信息需要全部被弹出栈帧,这些过程涉及到了栈的管理,需要CPU一定的开销。

2.寄存器保存于恢复开销:

        函数的调用过程当中少不了进行各种运算以及各种临时变量的使用,那么也就必然少不了用寄存器来存储各种临时变量和中间结果,但是在函数调用的时候,寄存器当中的值可能需要被保存到内存,需要给下一个调用的函数使用,函数执行结束以后再从内存当中恢复。

        针对从内存中恢复这一点我多提一句:当程序执行函数的时候、当前函数的执行状态包括寄存器当中的值是需要被保存的,以便在调用另一个函数之后能够恢复,这是因为在函数调用过程当中,新的函数可能会使用相同的寄存器来存储临时变量和中间结果,为了避免冲突,当前函数的寄存器当中的值通常要被保存到内存当中,然后在新的函数调用完成之后再从内存当中讲原来的函数数据恢复。

        这个过程涉及到了两个关键的步骤:保存和恢复。

        保存:在函数调用之前,当前函数的执行状态(包括寄存器当中的值)被保存到内存当中的某个位置,这确保了在调用其他函数的时候寄存器的内容不会被覆盖找不到或者意外修改。

        恢复:当调用的函数执行完成之后,需要将之前保存的执行状态给恢复过来,也包括寄存器当中的值,这样当前函数可以继续执行,并且在寄存器当中的值仍然是正确的。

        这个过程的目的是确保在函数调用的层次结构当中,每个函数都能够正常使用寄存器而不受其他函数的影响,这种保存和恢复的操作会引入一些开销,在性能优化的上下文当中,程序员通常需要考虑如何减少这些开销,以提高程序运行的效率。

        好了说到这里的时候可能还会有很多的小伙伴一头雾水,始终不明白,一个函数好端端地执行着,干嘛要保存起来去执行另外一个函数呢?有同学会认为这个过程是中断的过程,其实不然,这个过程类似于中断,但并不是中断,因为函数本身调用的过程当中是大概率会调用到另外一个函数的,注意是大概率不是一定。

        这个过程涉及到了函数调用和程序的控制流,虽然中断也涉及到了恢复和保存但是这里我们主要是在描述函数调用的情景。

        在程序执行过程当中函数之间的调用时非常常见的,一个函数执行好好的,但是程序可能需要执行其他功能或者子功能,这个时候就需要调用另外一个函数,在函数调用的过程当中,当前函数的执行状态(包括寄存器当中的值)需要被保存,以确保调用的函数能够被正常执行而不受之前的影响。

        我举一个例子,就比如我们所熟知的主函数 main(),它要调用一个名为Add的函数计算一些数据的和,在调用这个Add函数之前,主函数需要保存当前的执行状态包括寄存器当中的值,然后Add函数执行完毕之后,主函数需要恢复到之前的状态继续执行。代码如下:

#include"Yangon.h"int Add(int a, int b) {int c = a + b;return c;
}
int main() {int ret = Add(2, 4);cout << ret << endl;return 0;
}

3.参数传递开销

        参数传递的方式有值传递、引用传递还有指针传递,传递方式的不同开销也会不同,传递一些占用内存很大空间的对象的时候,可能会涉及到复制或者传递引用或者指针的开销。

4.栈帧生成开销

        函数的每一次调用都会生成一个新的栈帧,其中包含了局部变量、参数、返回地址等信息,栈帧的生成于销毁都会引入开销。具体的开销分为以下几个部分。

        生成栈帧开销:首先需要分配内存空间,系统会自动地位新的栈帧分配空间,这通常涉及到将栈指针移动到新的位置,并且在新的位置上为栈帧分配足够多的空间。分配好空间之后下一步就是初始化,栈帧内的一些信息,比如局部变量和参数、可能需要被初始化,这包括将参数传递到栈帧中,以及讲过局部变量的初始值设定为默认值或者特定的初始值。

        执行函数体:函数体当中的代码开始执行,操作局部变量、参数等信息、这不是栈帧生成本身的开销,但是在栈帧生成的过程当中函数体的执行也是一个开销。

        栈帧销毁开销:当函数执行完毕之后,系统需要立刻释放该函数的栈帧占用的内存空间,这通常涉及将栈指针移动会到调用函数最开始的位置,从而丢弃整个栈帧。但是在销毁栈帧之前可能会需要进行一些清理的工作,比如释放动态分配的内存,关闭文件包等。

        返回过程:如果这个函数又返回值、需要将返回值传递给调用函数、这可能涉及将返回值存储在特定的寄存器当中或者放置在约定好的内存之上。这些过程当中的每一步都会引入一些开销,尤其是在频繁地调用函数的场景当中。为了提高性能、编译器和程序员都需要进行一些优化策略,比如使用寄存器来传递参数和返回值减少不必要的栈帧操作。

5.寻址开销

        函数调用的时候需要寻找函数的入口地址,这可能涉及到符号表的查找或者跳转指令,引入一定的寻址开销。在程序执行的过程当中,当一个函数被调用的时候、需要找到该函数的入口地址,以便能够跳转到正确的位置执行函数体。

        符号表查找:编译时符号解析过程当中,编译器会生成符号表,其中包含了函数名和对应的入口地址,当程序执行的过程当中调用函数需要根据符号表来进行,以获取函数的入口地址。

        跳转指令:跳转到函数的入口,一旦找到函数的入口地址,程序需要执行跳转指令(例如函数跳转指令或者函数调用指令)来转移到函数的起始位置,这个过程可能涉及到将程序计数器或者指令寄存器的值设置为函数的入口地址。

        寻址开销影响:符号表查找和跳转指令可能均未命中,因为这些信息可能不再缓存当中,缓存未命中就会引入额外的开销,因为需要从内存当中加载相关信息。而且跳转指令也会影响流水线的性能,因为跳转会引入分支,现代处理器通常使用分支预测机制来减小分支带来的性能影响,但是如果分支预测失效也会引入一定的开销。(这部分的内容涉及到了计算机底层原理,如果有兴趣的小伙伴可以去看我之前的文章)。

        所以这个时候为了减少开销,编译器和硬件都需要采用一些优化策略,例如使用相对寻址而不是绝对寻址,采用间接寻址等。

6.代码段切换开销

        如果这个函数不再当前执行的代码段当中,可能需要进行代码段的切换,这也会引入一些开销。代码段切换通常指的是当前程序从一个代码段切换到另外一个代码段时所引入的开销,这种切换通常发生在函数调用或者模块加载等场景当中,下面详细解释一下。

        加载新的代码段:当程序执行到一个新的函数或者模块的时候,可能需要将新的代码段加载到内存当中去,这就涉及到从磁盘或者从其他存储介质当中读取代码,并且将其加载到可执行内存区域,代码加载通常伴随着地址映射和分页操作,确保代码在内存当中的正确位置,并且可以被正确访问。

        切换代码段:当程序从一个代码段切换到另外一个代码段时,需要调整程序计数器或者指令寄存器的值,以指向新代码的入口地址

        缓存效应:切换代码段可能导致缓存未命中,因为新的代码段的指令和数据有可能不再缓存当中,这也会引入额外的开销,因为需要从主内存当中加载新代码段的内容。

7.上下文切换的开销

        在多线程或者多任务环境当中由于上下文的切换必然会引入额外的开销,首先我们要知道什么是上下文切换,上下文切换是指在多任务(或者多线程当中)环境当中从一个任务切换到另一个任务时,保存当前任务的执行状态,包括寄存器的值,程序计数器的值等,并加载另一个任务的执行状态,这种切换会引入额外的开销。我下面为大家进行详细地讲解。

        保存当前上下文:在进行上下文切换之前,系统需要保存当前任务(线程)的执行状态,这包括寄存器的内容,程序计数器的值,堆栈指针等,这些信息通常保存在任务的上下文块当中。

        加载新的上下文:切换到另一个任务的时候,系统需要加载该任务的执行状态,这涉及到将寄存器、程序计数器的值从任务的上下文块当中恢复,以确保任务能够从上次中断的地方继续开始执行。

       调度开销:上下文的切换通常伴随着调度操作,即选择下一个要执行的任务、调度器需要考虑任务的优先级,时间片等信息,这也会引入一些开销。

       用户态和内核态的切换:在多任务环境当中,上下文切换可能涉及到用户态和内核态之间的切换,这通常需要额外的指令和特权级的转换。

        这里涉及到了很多操作系统的知识,这不是我今天要讲的重点所以就不提了,另外多说一句,任务指的是进程或者线程,而上下文的意思是值任务或者程序在执行的过程当中的当前状态和环境,这个状态包括了一系列的信息,使得程序或者任务被暂停的时候,可以保存当前的执行状态,以便之后能够正确地恢复和执行,上下文的内容通常包括寄存器的内容、内存的状态(堆、栈、数据区等信息)、处理器状态、文件描述符和I/O状态吗,其他状态信息。

8.内存访问开销

        当函数调用的时候、可能会引起新的内存访问、例如访问函数体内的局部变量,参数等,这会影响缓存的命中率。内存访问开销指的是在程序执行过程当中由于函数调用而引起的内存读取或者写入操作带来的可能的开销,这一过程可能包括对局部变量、参数、堆栈全局变量等的内存访问,对缓存的影响是其中一个关键因素,下面我为大家详细介绍关于内存访问开销的一些相关因素。

        局部变量和参数:当函数被调用的时候,局部变量和参数通常存储在栈上,访问这些局部变量和参数可能导致缓存未命中,因为这些参数不再缓存当中,如果局部变量被频繁地访问,但是又没有很好地利用缓存可能会导致缓存的命中率下降、增加访问内存的开销,因为缓存虽然容量小但是速度远快于内存。

        栈操作:函数调用和返回的过程当中,栈上的数据被频繁地读写,这可能导致缓存行的失效,特别是如果相邻的栈帧被频繁地读写。

        全局变量和静态变量:全局变量和静态变量通常存储在数据段或者BSS段当中、访问这些变量可能会引起缓存未命中尤其是在全局变量较多的程序当中。数据段和BSS段都是静态区当中的区域,数据段用于存储已经初始化的全局变量而BSS段则用于存储未初始化的全局变量和静态变量,因为这些数据存储于静态区,而静态区是内存的空间,所以访问这些变量的时候可能就会引起缓存未命中,这意味着处理器在尝试访问这些变量的时候发现它们不再缓存当中,许需要从内存当中加载,这可能就会导致额外的访存延迟,影响程序的性能,特别是在又大量的全局变量的程序当中。

        堆操作:如果函数调用导致对堆内存的操作(如动态内存分配和释放),这也可能会影响缓存的性能,频繁的堆操作可能导致缓存行失效,尤其是在多线程的环境当中

9.返回值传递开销

        返回值的传丢方式不同也会影响开销,返回较大的对象的时候可能会需要进行复制或者返回引用或者指针,这里接涉及到了值传递和引用传递还有址传递。

10.函数调用的指令开销

        生成和执行函数调用的指令本身也会需要一些开销。在大多数情况下这些开销相对较小,但是在对性能要求较高的场景中,可以考虑使用内联函数避免过多的函数调用,接下来我就为大家详细讲解内联函数做了什么。


二、内联函数省略掉了哪些开销

1.函数调用开销

        内联函数会在编译的时候将函数体的代码展开插入到调用处,而不是通过函数调用的方式,这避免了函数调用和返回时的栈帧生成与开销。

2.参数传递开销

        内联函数可以避免一部分参数传递的开销,因为参数值直接被插入到调用点,而不需要通过栈或者寄存器传递。

3.寄存器保存与恢复开销

        内联函数的调用避免了函数调用时候的栈帧生成,因此不需要保存和恢复寄存器当中的值。

4.代码段切换开销

        内联函数已经将代码段直接插入到了调用处、不需要进行代码段的切换。

5.指令开销

        内联函数避免了生成和执行函数调用的指令本身的开销。

6.返回值传递开销

        返回值也可以直接插入到调用点,减少一次拷贝的开销。

7.函数调用指令的开销

        函数体直接插入到了调用点,自然也就不需要函数调用的指令开销了。

8.内存访问开销

        内联函数可以减少内存访问的开销,因为局部变量和参数直接嵌入到了函数调用点。


 三、内联函数到底做了什么 

        到了这里可能已经又很多小伙伴明白了,所谓的内联函数顾名思义,就是编译器在编译的时候在函数的调用处直接将函数体展开,这样可以省却很多的开销,很多短小却频繁调用的函数需要使用内联函数进行优化,内联函数的关键字是inline。

#include <iostream>
using namespace std;inline int Add(int a,int b){int c = a + b;return c;
}
int main()
{int ret = 0;for(int i = 0;i <= 100; i++){ret += Add(i,i+1);}return 0;
}

        例如这个地方这个函数如果内部并不复杂而且多次被调用的话,建议使用内联函数进行优化,但是并不是程序员只要使用inline修饰,编译器就一定会将这个函数当作内联函数来处理,这个关键字对于编译器来说知识一个建议,因为内联函数并不是尽善尽美,因为每一次遇到内联函数都要将其展开的话,我们编译生成的目标文件就会变得膨胀冗余,所以内联函数也要合理使用。


总结

        这篇文章主要为大家讲解了函数调用所需要的开销以及内联函数的功能,内联函数可以为函数的调用节省掉许多的开销,这篇文章涉及到了很多关于函数调用的知识,这其中又穿插了很多关于操作系统和计算机组成原理的相关知识,不管是考研还是就业这部分的知识都是非常有用的,希望能够帮助到大家!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/233017.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

JJJ:组合数据类型2

文章目录 字典的创建和删除 p50字典的创建方式 字典元素的访问及遍历 p51字典操作的相关方法 p52字典生成式集合的创建与删除 p54集合的操作符 p55集合的操作方法、集合的遍历 p56列表、元组、字典、集合的区别 Python 3.11新特性结构模型匹配字典合并运算符 |同步迭代 字典的创…

element-ui 抽屉里面嵌套弹窗

当我们在element-ui 的Drawer 抽屉里面嵌套弹窗时&#xff0c;有时会出现关闭弹窗后&#xff0c;抽屉依然被遮罩层挡着的情况&#xff0c;解决方法是 在 Drawer 里面写 :append-to-body"true" 和 :close-on-click-modal"false"&#xff0c;在弹窗里面写 :a…

vue中的事件修饰符、表单双向数据绑定和计算属性

目录 一、事件修饰符 二、表单双向数据绑定 模拟双向数据绑定&#xff08;双向数据绑定底层原理&#xff09; 三、计算属性 计算属性和methods方法区别&#xff1f; 计算属性和watch区别&#xff1f; 一、事件修饰符 stop 阻止事件冒泡 prevent 阻止事件默认行为 ca…

Java常见原子性操作

在Java语言中&#xff0c;对基本数据类型的变量读取赋值操作都是原子性的&#xff0c;对引用类型的变量读取和赋值的操作也是原子性的&#xff0c;因此诸如此类的操作是不可被中断的&#xff0c;要么执行&#xff0c;要么不执行&#xff0c;正所谓一荣俱荣一损俱损。 原子操作…

Linux线程——互斥锁

概念 互斥量&#xff08;mutex&#xff09;从本质上来说是一把锁&#xff0c;在访问共享资源前对互斥量进行加锁&#xff0c;在访问完成后释放互斥量上的锁。对互斥量进行加锁后&#xff0c;任何其他试图再次对互斥量加锁的线程将会被阻塞直到当前线程释放该互斥锁。 如果释放…

【HCIP学习记录】OSPF之DD报文

1.OSPF报文格式 24字节 字段长度含义Version1字节版本&#xff0c;OSPF的版本号。对于OSPFv2来说&#xff0c;其值为2。Type1字节类型&#xff0c;OSPF报文的类型&#xff0c;有下面几种类型&#xff1a; 1&#xff1a;Hello报文&#xff1b;● 2&#xff1a;DD报文&#xff1…

美国联邦机动车安全标准-FMVSS

FMVSS标准介绍&#xff1a; FMVSS是美国《联邦机动车安全标准》&#xff0c;由美国运输部下属的国家公路交通安全管理局(简称NHTSA)具体负责制定并实施。是美国联邦政府针对机动车制定的安全标准&#xff0c;旨在提高机动车的安全性能&#xff0c;减少交通事故中的人员伤亡。F…

ubuntu无 root 权限安装 screen

网上的方法主要是如下图的方法&#xff0c;源码安装&#xff0c;但是我一直 make install失败显示没有权限 然后选择放弃&#xff0c;然后随便试了一下方法 2&#xff0c;成功 方法 1 方法 2 pip3 install screen结果&#xff1a;

生物识别应用指纹的算法是什么样的?有什么性能?

方案特点 • 采用金融级安全芯片 ACH512 的指纹模组&#xff0c;指纹和密码安全存储&#xff0c;云端数据安全传输 • 采用高性能指纹专用安全MCU芯片ACM32FP4&#xff0c;支持小点阵图像算法处理 • 支持80*64、88*112、96*96、160*160、192*192等像素传感器 • 已适配传…

Ubuntu系统使用Nginx搭建RTMP服务器

环境&#xff1a; 推流端 rockpi s 主控rk3308 运行ubuntu系统 服务端 ubuntu 播放器 VLC播放器 服务端安装依赖&#xff1a; apt-get install build-essential libpcre3 libpcre3-dev libssl-dev创建nginx编译目录&#xff1a; mkdir my_nginx_rtmp cd my_nginx_rtmp/下载 …

【Python基础】文件读写

文章目录 [toc]打开文件open()函数参数解析示例 文件路径绝对路径示例 相对路径示例 打开文件的模式常用模式 读文件示例 写文件示例 按行读写文件readline()示例 readlines()示例 writelines()示例 关闭文件示例finally语句示例 上下文管理器示例 自定义读写类示例 打开文件 …

计算机网络:物理层(编码与调制)

今天又学会了一个知识&#xff0c;加油&#xff01; 目录 一、基带信号与宽带信号 1、基带信号 2、宽带信号 3、选择 4、关系 二、数字数据编码为数字信号 1、非归零编码【NRZ】 2、曼彻斯特编码 3、差分曼彻斯特编码 4、归零编码【RZ】 5、反向不归零编码【NRZI】 …

查找Apple Watch的序列号有重要意思,主要有两种方法

如果你打算购买二手Apple Watch&#xff0c;你可能需要检查它的序列号或IMEI号&#xff0c;来确保可靠性。以下是如何从Apple Watch和iPhone中查找序列号。 在Apple Watch上查找序列号和IMEI 1、在Apple Watch上&#xff0c;按下手表表面的数字皇冠以打开应用程序网格或列表。…

【设计模式--行为型--访问者模式】

设计模式--行为型--访问者模式 访问者模式定义结构案例优缺点使用场景扩展分派动态分派静态分派双分派 访问者模式 定义 封装一些作用于某种数据结构中的各元素的操作&#xff0c;它可以在不改变这个数据结构的前提下定义作用于这些元素的新操作。 结构 抽象访问者角色&…

晶体管的工作状态判断和工作条件

晶体管是模拟电路中基础的器件&#xff0c;对于电子工程师来说&#xff0c;了解晶体管工作的条件和判断晶体管的工作状态都是非常基础的&#xff0c;本文将带大家一起学习或回顾一下。 一、晶体管工作的条件 1.集电极电阻Rc&#xff1a; 在共发射极电压放大器中&#xff0c;…

在Linux中遇到“没有可用软件包”的情况

1. 旧的或不完整的软件源 更新你的软件源列表 对于Ubuntu/Debian系统 sudo apt-get update 对于RHEL/CentOS系统&#xff0c;使用命令 sudo yum update 或 sudo dnf update&#xff08;取决于你的系统版本&#xff09; 添加其他软件源&#xff1a; 例如&#xff0c;你可以安…

HPM6750系列--第十篇 时钟系统

一、目的 上一篇中《HPM6750系列--第九篇 GPIO详解&#xff08;基本操作&#xff09;》我们讲解了HPM6750 GPIO相关内容&#xff0c;在进一步讲解其他外设功能之前我们有必要先讲解一下HPM6750的时钟系统。 时钟可以说是微控制器系统中的心脏&#xff0c;片上外设模块必须依赖时…

爱普生手机打印助手Epson Smart Panel下载分享

新一代智能打印Epson Smart Panel为您提供一站式打印服务&#xff0c;人性化装机助手&#xff0c;智能联网&#xff0c;快速开启远程微信打印&#xff0c;全新交互式设计&#xff0c;打印体验焕新升级&#xff1b; 根据手机不同可选下列不同下载方式&#xff1a; 人性化智能装机…

《每天一分钟学习C语言·一》

1、转义字符&#xff1a;\n换行&#xff0c;\t前进一个tab键&#xff0c;\b退格键 2、八进制前面有0&#xff0c;%o或者%#o表示八进制&#xff0c;十六进制前有0X&#xff0c;%0x或者%#0x表示十六进制 3、%u打印无符号数&#xff0c;%g显示小数&#xff0c;类似于%f&#xff…

给机器安装ubuntu

准备材料: 1.镜像iso&#xff08;地址:清华镜像源https://mirror.tuna.tsinghua.edu.cn/ubuntu-releases/20.04/ubuntu-20.04.6-live-server-amd64.iso 2.U盘&#xff08;4gb就行&#xff09; 3.制作启动盘工具&#xff08;地址:https://clone-audio-dataset-1309770014.co…