| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 系统运维 -> 《C++性能优化指南》 linux版代码及原理解读 第七章 优化热点语句 -> 正文阅读 |
|
[系统运维]《C++性能优化指南》 linux版代码及原理解读 第七章 优化热点语句 |
? 语句优化一般来说是从执行流中移除指令的过程,这个是针对单一的语句的。 语句优化的问题在于,如果针对的语句不是函数调用而起其他的语句,这种语句的本身消耗的就很少。我们在进行语句优化的时候应该关注以下几种情况:
在嵌入式这种小型处理当中,对语句的优化效果可能会比较明显,但是在桌面级或者更高级的处理器上,有很多中硬件级别的优化,包括指令级别的并发和缓存等,这导致单一的一条语句造成的影响要小于内存操作造成的影响。 在进行语句优化的时候要注意编译器,不同的编译器、不同的版本、不同的优化方式都会造成不同的优化程度甚至反向优化。 .1 优化循环中的代码原始代码如下 :
for(part 1 ; part 2 ; part 3 ) 语句包含了三部分,第一部分是初始化语句,第二部分是从每次循环开始判断是否执行本次循环内的语句。第三部分是每次执行完成之后运行的。 从这里我们可以看到,part 2 是会一直运行的,如果在这部分当中包含了一个运行开销比较大的操作,那这部分就会造成很多的运行消耗。可以看到在debug模式下的运行输出 缓存条件语句如果我们把part 2 当中的sizeof操作符号放到part 1 当中,代码如下:
可以看到在debug模式下的运行结果
但是如果在release模式下,实际上这两种方式运行结果是相同的。
这是因为编译器实际上为我们进行了优化。如果我们把编译器的优化等级调低的话,
那么结果就是
可以看到,在优化选项不同的时候,效果会不同。 更改循环方式基本上说,for循环在编译后会编程 if / goto/类似的语句,这种跳转语句可能会降低运行速度,如果我们更换一种循环方式的话
结果如下:
移除不变性代码像刚才我们把for循环的循环判断部分中的代码移除到初始化部分中,这种不会改变的代码(在这种情况下sizeof的结果是不会改变的)称为不变性代码,将这种代码移动到循环之外是一种常见的优化方式。 从循环中移除隐含的函数调用C++代码可能会隐式地调用函数,而没有这种很明显的调用语句。以下是几种情况: 将循环放入函数以减少调用开销如果程序需要访问一堆的数据,但是它在访问单个数据的时候需要调用函数进行处理当前的数据,那么在这个流程当中,会不停的触发函数调用的开销 。如果我们将循环中调用函数这种模式改成在调用函数当中运行循环,这样的一种方式称为循环倒置。 看如下代码:
输出结果:
将函数replace_nonprinting的调用移除,改成如下代码:
输出结果:
可以看到有一部分的优化效果。 优化频繁的检测假设在一个event loop中,这个事件循环运行在主线程当中,他可以每秒执行1000个事物。如果我们要处理一个终止命令,这个判断需要多长时间判断一次呢? 首先是鼠标按键按下之后,如果我们立即检测事件队列,这时候我们一定会发现那个鼠标事件吗?答案是否定的。
那如果像下面这样,我们在每次的事件循环当中都去调用poll_for_exit(),它会花费50ms来检测是否有退出键按下,如果按下就调用exit_program(),这将会花费400~600毫秒来停止程序。
现在我们给程序加一个响应时间。假设我们认为用户对程序终止的响应时间在1s以内是完全可以接受的,程序的终止操作需要消耗400到600ms来处理,那现在我们应该如何处理这个终止事件呢?还需要每次都判断终止事件吗? 做一个简单地数学计算: 1s - 600 ms = 400 ms ; 只要我们在指令传过来之后的400ms之内开始执行exit_program()函数,这样就会在1秒钟之内完全结束掉程序。也就符合用户的预期。 那我们就需要将程序修改一下:
其他优化技巧对于for循环的优化,网上有很多其他的优化技巧,比如展开循环、openmp或者其他的一些优化方式。感兴趣的可以上网上搜索一下。 .2 从函数中移除代码函数调用每次程序调用一个函数时,都会发生类似下面这样的处理(依赖于处理器体系结构和优化器设置)。 (1) 执行代码将一个栈帧推入到调用栈中来保存函数的参数和局部变量。 (2) 计算每个参数表达式并复制到栈帧中。 (3) 执行地址被复制到栈帧中并生成返回地址。 (4) 执行代码将执行地址更新为函数体的第一条语句(而不是函数调用后的下一条语句)。 (5) 执行函数体中的指令。 (6) 返回地址被从栈帧中复制到指令地址中,将控制权交给函数调用后的语句。 (7) 栈帧被从栈中弹出。 从流程上看,函数调用有一定的开销。当频繁的调用某个函数时,单纯调用函数的开销就会变大,参考之前循环倒置的部分。不过从某些情况下,函数调用也可能会有一些好处。比如和那些被内联展开(inline )的大型的函数来说,通常这种函数会更加紧凑一些。同时紧凑的函数有利于提高缓存和虚拟内存的性能。参考前面讲的内存页抖动、以及缓存名中率部分。 函数调用的基本开销
除了计算参数表达式的开销外,复制每个参数的值到栈中也会发生开销。 成员函数调用(与函数调用) 每个成员函数都有一个额外的隐藏参数:一个指向this类实例的指针,而成员函数正是通过它被调用的。这个指针必须被写入到调用栈上的内存中或是保存在寄存器中。
函数调用和返回对程序功能的实现没有影响,同样我们可以将函数调用去掉,直接把函数体中的代码替换函数的调用来避免单纯的函数调用的开销。许多编译器对尝试使用这样的方式,即通过内联的方式进行优化。
当调用函数的时候,下一个执行地址EIP(指令指针)会被保存到栈中,函数返回时会将这个数据取出,然后从这个地址的指令继续向下运行。不过这几次都涉及到跨越非连续地址,可能会造成流水线停顿或者高速缓存未命中。 虚函数的开销C++中,除了构造函数不能被定义为虚函数之外,其他的成员函数都可以被写成虚函数。带有虚函数的实例是通过添加一个指向虚函数表的指针来实现的,这个表中有虚函数的函数地址。虚函数表指针通常都是类实例的第一个字段,这样解引时的开销更小。 调用虚函数的时候有两次解引用操作。首先通过虚函数表的指针获得表的地址,其次通过虚函数表的初始地址加偏移量,解引用获得函数的真实执行地址。这两次操作都是非连续的内存操作,而每次这种非连续的内存操作都会增加流水线停顿和高速缓存未命中的概率。 虚函数的另一个问题是编译器难以内联它们。编译器只有在它能同时访问函数体和构造实例的代码(这样编译器才能决定调用虚函数的哪个函数体)时才能内联它们。 继承中的成员函数调用
如果继承关系最顶端的基类没有虚成员函数,那么代码必须要给this类实例指针加上一个偏移量,来得到继承类的虚函数表,接着会遍历虚函数表来获取函数执行地址。这些代码会包含更多的指令字节,而且这些指令通常都比较慢,因为它们会进行额外的计算。这种开销在小型嵌入式处理器上非常显著,但是在桌面级处理器上,指令级别的并发掩盖了大部分这种额外的开销。
代码必须向this类实例指针中加上一个偏移量来组成指向多重继承类实例的指针。这种开销在小型嵌入式处理器上非常显著,但是在桌面级处理器上,指令级别的并发掩盖了大部分这种额外的开销。
对于继承类中的虚成员函数调用,如果继承关系最顶端的基类没有虚成员函数,那么代码必须要给this类实例指针加上一个偏移量来得到继承类的虚函数表,接着会遍历虚函数表来获取函数执行地址。代码还必须向this类实例指针加上潜在的不同的偏移量来组成继承类的类实例指针。这种开销在小型嵌入式处理器上非常显著,但是在桌面级处理器上,指令级别的并发掩盖了大部分这种额外的开销。
为了组成虚多重继承类的实例的指针,代码必须解引类实例中的表,来确定要得到指向虚多重继承类的实例的指针时需要加在类实例指针上的偏移量。如前所述,当被调用的函数是虚函数时,这里也会产生额外的间接开销。 后续会写博客专门 描述这几中情况的具体实现。 函数指针的开销
函数调用开销总结
善于使用内联函数移除函数调用开销的一种有效方式就是内联函数。以下几种情况编译器通常会主动将函数进行内联: 在类的定义中同时定义的函数会被隐式内联。比如如下:
通过将在类定义外部定义的函数声明为存储类内联,也可以明确地将它们声明为内联函数。 此外,如果函数定义出现在它们在某个编译单元中第一次被使用之前,那么编译器还可能会自己选择内联较短的函数。 在使用之前定义函数?? 移除未使用的多态性当基类中的某些虚函数在派生类中没有实现,或者永远不会实现的时候,这个时候移除virtual关键字可以提高调用的速度。 优化接口的实现方式。一般来说我们在定义接口的时候,都是写一个由纯虚函数组成的抽象基类,然后将这个头文件提供给调用方,我们实现一个类通过继承这个抽象基类来实现具体的功能。比如:
然后根据不同的平台,可以我们会写出如下的代码:
有时候,如果我们只实现了一种接口,比如只实现了windows的接口,那我们可以把其中的virtual 关键字去掉直接在file.h文件中实现。尤其是对于GetChar()这样的函数,每次获取一个字符都要调用一次虚函数的解析。 在链接时选择接口实现如果无需在运行时做出选择的话,那么开发人员可以使用链接器来从多个实现中选择一种。具体做法是不声明C++接口,而是在头文件中直接声明(但不实现)成员函数,就像它们是标准库函数一样:
在windowsfile.cpp文件中有如下Windows的实现代码:
同理在linux下实现linux.cpp,在其中将代码定义好。 这样做的方法,通过Cmake或者Qmake甚至Makefile等等项目管理的方式,我们可以指定程序具体链接哪个cpp文件,从而选择一种特定的实现方式。 在编译时选择接口实现还有一种方式是将linux和windows的实现方式都在file.h中实现,但是通过宏来控制,类似下面的方式:
用模板在编译时选择实现模板的特性是一把双刃剑:一方面,即使开发人员在某个模板特化中忘记实现接口了,编译器也不会立即报出错误消息;但另一方面,开发人员也能够选择不去实现那些在上下文中没被用到的函数。从性能优化的角度看,多态类层次与模板实例之间的最重要的区别是,通常在编译时整个模板都是可用的。在大多数用例下,C++都会内联函数调用,用多种方法改善程序性能 避免使用PIMPL惯用法PIMPL是“Pointer to IMPLementation”的缩写,它是一种用作编译防火墙——一种防止修改一个头文件会触发许多源文件被重编译的机制——的编程惯用法。
要实现PIMPL,开发人员要定义一个新的类,在本例中,我们将其命名为Impl。bigclass.h的修改如代码所示。
移除对DDL的调用一种改善函数调用性能的方式是不使用DLL,而 是使用对象代码库并将其链接到可执行程序上。 使用静态成员函数取代成员函数实际上每次调用类的成员函数时,都会有一个隐式参数this指针被传递进去,而这个this指针就是这个类实例的地址。之所以有这么一个参数可以参考前面的虚函数部分,通过这个this指针加上偏移量,我们可以获得类成员的数据,解引this指针获取虚函数表指针。 从这里我们可以看到,如果一个成员函数不需要处理类内部的数据,也不需要其他的虚函数的话,这个this指针在这里没有什么意义。我们可以将这种函数声明为静态函数,这样就可以避免这种问题。因为同一个类的静态成员函数共享一个函数地址,所以没有this指针。 将虚析构函数移至基类中任何有继承类的类的析构函数都应当被声明为虚函数。这是有必要的,这样delete表达式将会引用一个指向基类的指针,继承类和基类的析构函数都会被调用。 确保在这个基类中至少有一个成员函数,可以强制虚函数表指针出现在偏移量为0的位置上,这有助于产生更高效的代码。 .3 简化表达式多项式 在C++中可以写为:
这条语句将会执行6次乘法运算和3次加法运算。 我们可以根据霍纳法则重复地使用分配律来重写这条语句:
这条优化后的语句只会执行3次乘法运算和3次加法运算。 将常量组合在一起
但是如果改成这样编译器就无法进行像刚才那样进行优化:
用更高效的运算符有些数学运算符在计算时比其他运算符更低效。例如,整数表达式x*4可以被重编码为更高效的x<<2。 另一种优化是用位移运算和加法运算替代乘法。例如,整数表达式x9可以被重写为x8+x*1,进而可以重写为(x<<3)+x。 使用整数计算替代浮点型计算在PC上面浮点型数据的计算开销是昂贵的,但是整型计算的速度比浮点计算快起码一个数量级 以上。 双精度类型可能会比浮点型更快某些编译环境下,双精度类型的计算速度比浮点类型的计算速度更快。 用闭形式替代迭代计算有时候我们会遇到一些问题使用时间复杂度O(n)的算法进行迭代求解,但是如果我们能转换思路,找到求解的更快的方法,这种方法的计算时间为常量,不进行任何迭代,这种方法称为闭形式。 具体的可以查找常量时间复杂度算法,后续我会写相关博客讨论相关算法。 .4优化控制流程惯用法之前提到过,由于当指令指针必须被更新为非连续地址时在处理器中会发生流水线停顿,因此计算比控制流程更快。C++编译器会努力地减少指令指针更新的次数。了解这些知识有助于我们编写更快的代码。 用switch替代if-elseswitch语句的参数实际上是一个常量,然后case中的很多个选项也是常量,这样switch语句会被编译程jump指令表,jump的索引值就是要测试的那个数值,case的数值也会被编译器进行排序,这样在理想情况下,case中的数值是连续的或者近似连续的,那么每次比较处理的开销都是O(1)。如果case中的选项跨度很大而且不连续,那么jump的指令表会很大,但是编译器可能会将其优化成适合二分查找的结构,这样最差的时间开销就是O()。 用虚函数替代switch或if使用无开销的异常处理
不要使用异常规范
? |
|
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年12日历 | -2024/12/30 2:28:01- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |