前言
任何优秀的大软件里面都是一个优秀的小程序。
正文
当前阅读进度
P79
第一章 优化概述
-
高德纳:
我们应当忘记小的性能改善,大部分过早优化都是万恶之源
。
感悟:小的性能优化,有可能会浪费大量的时间,只有在那种使用频率很高,且耗时很严重的,绝对是有必要进行优化的。 -
编写高效代码和编写低效代码所需要花费的时间是一样的,为什么还有人特意去编写低效代码呢?
-
程序中只有10%的代码的性能是很重要的。
-
常识可能是性能改善最大的敌人。
-
性能惯犯: 函数调用,内存分配,循环。
-
记得打开编译器的优化选项。
-
编写小的成员函数去访问成员变量是一种优秀的C++编码风格。
-
改善程序性能的几个重要技巧:
- 预计算:将计算从运行时移动至链接、编译或是设计时。
- 延迟计算: 若当前计算结果不会被使用,那么将计算推迟到真正需要使用计算结果时。
- 缓存: 节省和服用昂贵的计算。
-
减少对内存管理器的调用是一种非常有效的手段。
-
减少内存分配和复制。
-
现代编译器的局部改善方面的优化已经做的非常优秀了,不一定要将整个代码的i++替换成++i.
-
提高程序的并发性,并用好用于同步并发线程而可以让他们共享数据的工具。
-
用好编译器:记得打开编译器的优化选项。
https://www.qt.io/zh-cn/blog/2018/12/03/modern-qt-development-top-10-tools-using 有空要了解一下这里面的工具,这是对你自己的扩展。 -
编写许多小的函数去访问各个类的成员变量是一种优秀的C++编码风格。
-
对代码优化而言,学习和使用查找和排序的最优算法才是康庄大道。
第二章 影响优化的计算机行为
-
c++ 11提供了std::atomic<>的特性,可以让内存在一段短暂的时间内表现得仿佛是字节的简单线性存储一样。
-
访问包含连续地址的数据结构(如数组或矢量),要比访问包含通过指针链接的节点的数据结构快,因为连续地址所需的存储空间更少。
-
虚拟内存只是制造了拥有充足的物理内存的假象。
-
冯诺依曼瓶颈: 通往主内存的接口是限制执行速度的瓶颈。
-
一次非对齐的内存访问相当于这些字节在同一字中时的两倍。
-
读取一个不在高速缓存中的字节会导致许多邻近的字节也都被缓存起来了。
-
跳转指令或跳转子例程指令会将执行地址变成一个新的值,在执行跳转指令一段时间后,执行地址才会被更新。
-
线程的切换会出现什么?:为即将暂停的线程保存处理器中的寄存器,然后为即将被继续执行的线程加载之前保存过的寄存器。
-
当程序的切换会发生什么?
所有脏的高速缓存界面都必须被刷新至物理内存之中。
所有的处理器寄存器都需要被保存。
“物理地址到虚拟地址的映射关系都需要被保存。” 页表
-
访问线程间的共享数据,要比访问非共享数据要慢的多。
-
系统调用的开销是单线程程序中的函数调用开销的数百倍。
-
微处理器的内存控制逻辑可能会选择延迟写入内存以优化内存总线的使用。
-
计算比做决定更快。
第三章 测量性能
-
伽利略: 测量可测量之物,将不可测量之物变为可测量。
-
90/10法则: 一个程序花费90%的时间执行其中10%的代码。
-
如果被优化的代码在程序整体运行时间中所占得比率不大,那么即使对它的优化非常成功,也是不值得的。
-
在测量性能时,要去思考: 为什么这些代码是热点?
-
在测试的过程中,修改后,可以用笔记录下每次测试运行的时间,通过多次的比对,就可以很明显的了解哪次的修改是有效的,并且需要记录下此次的修改,这样有利于后面的整体比较。
-
优化工作受两个数字主导: 优化前的性能基准测量值和性能目标值。测量性能基准不仅对于衡量每次独立的改善是否成功非常重要,而且对于向其他利益相关人员就优化成本开销做出解释也是非常重要的。
-
性能测试项目清单:
a . 启动时间: 从用户按下回车键直至程序进入主循环处理循环所经过的时间。其实主要就是从main()函数的开头->进入主循环的时间。
b. 退出时间: 从用户点击关闭图标或是输入退出命令直至程序实际完全退出所经过的时间。通常,开发人员可以通过测量主窗口接收到关闭命令到程序退出 main() 的时间来得到退出时间。因为重启一个服务或是长时间运行的程序所需的时间等于它的退出时间加上它的启动时间。
c. 响应时间: 执行一个命令的平均时间或最长时间. 低于 0.1 秒:用户在直接控制 0.1 秒至 1 秒:用户在控制命令 1 秒至 10 秒:计算机在控制 高于 10 秒:喝杯咖啡休息一下.
d. 吞吐量。 吞吐量表述为在一定的测试负载下,系统在每个时间单位内所执行的操作的平均数。
-
什么是分析器?
分析器是一个可以生成另外一个程序的执行时间的统计结果的程序。分析器可以输出一份包含每个语句或函数的执行频度、每个函数的累积执行时间的报表。 -
测量运行时间是一种测试 “如何减少某个特定函数的性能开销”的假设的有效方式。
-
访问内存的开销远比其他指令的开销大。
-
字符串连接运算符的开销很大,因为为了连接会创建一个临时字符串,这就会调用很多次内存管理器来为临时字符串分配内存。
-
写时复制:也称为隐式共享,将内部资源的复制推迟到第一次写入的时候,当修改时,再执行复制操作。
-
使用复合赋值操作避免临时字符串,就类似于:
result = result +s[i];
这种就会产生临时字符串,如果i很大的话,就更夸张了,性能会降低很多。而如果使用result += s[i];
的话,那么就移除了所有为了分配临时字符串对象来保存连接结果而对内存管理器的调用。总而言之,使用复合赋值操作,减少了对内存管理器的调用。
-
通过预留存储空间减少内存的重新分配。也就是使用reserve 可以移除字符串的重新分配,还可以改善所读取数据的缓存局部性,使我们从中得到更好的改善效果。
-
在使用形参传递字符串时,如果直接传递,函数通常会复制出一个形参字符串。这也会浪费一定的效率。所以,我们将
std::string str
修改为std::string const & s
这样,会给函数一个常量引用作为参数,无法更改,也就不会被复制。
但其实,如果单单更改这个,是不会出现性能的优化的,原因在于形参传递时,传入的是一个指针,而这样的话,每次函数进入都需要进行解引用。推测这些额外的开销,可能足以导致性能的下降。 所以,这就引出了第16点。
-
解决方法是使用迭代器。 字符串迭代器是指向字符缓冲区的简单指针。使用迭代器可以节省两次解引用操作。
这种方法还有一个好处就是,用于控制for循环的s.end()的值会在循环初始化时被缓存起来,这样可以节省一定的开销。而不用每次都调用Length。 -
消除对返回值的复制。在函数返回值时,C++是有可能出现调用复制构造函数将处理结果设置到调用的上下文的。
-
**当程序有极高的性能要求时,可以不适用c++标准库,而是利用C风格的字符串函数。**注意:除了部分限制极其严格的嵌入式环境中,在栈上声明最差情况下的缓冲区为1000甚至10000个字符是没有问题的。
这会比上一个版本快解决6倍。但需要自己管理临时存储空间,这是容易出错的。 -
使用更好的算法,减少for循环的次数。
-
使用c++自带的函数优化更好的算法。 0.65
-
不创建新的字符串,而是修改参数字符串的值作为结果返回。 0.81
-
string_view:包含一个指向字符串的无主指针和一个表示字符串长度的值。
-
如果一个团队觉得需要对他们使用的字符串做出一些改变的话,那么最好在设计之初,就定义好全工程范围内的typedef:
typedef string MyString
- 一般来说,如果使用了string作为返回值,那么最好不要出现还要将这个返回值进行回转的情况,也就是再重新转为char* 的情况,因为很可能你在函数内部就已经做了一次将char* -> string的动作了。后面又转回去,无异于是浪费时间了。
举个例子:
string MyClass::Name const()
{
return "MyString";
}
就上面这个例子,其实就已经做了一次将char* ->string的转换了。
一般来说,建议将返回值弄成char* ,在需要的时候,再转就好了,不要做过多无意义的转换。
顺便说下,string 转char* : char * str = str.c_str();
- 将字符串作为对象而非值可以降低内存分配和复制的频率。
- 将函数的结果通过输出参数作为引用返回给调用方法会复用实参的存储空间,这可能比分配新的存储空间更有效。
第五章-优化算法
-
二分算法就是一种常用的具有O(logn)的算法。
-
当多种线性时间算法合并在一起的时候,可能会导致他们的时间开销变为O(N2).
-
优化模式:
1、 预计算:通过在热点代码前来执行计算来将计算从热点部分中移除。
2、延迟计算:通过在真正需要执行计算时才执行计算,将计算从某些代码路径上移除
3、 批量处理:每次度多个元素一起进行计算,而不是一次只对一个元素进行计算。
4、 缓存:通过保存和复用昂贵计算的结果来减少计算量。
5、特化:通过移除未使用的共性来减少计算量。
6、 提高处理量:通过一次处理一大组数据来减少循环处理的开销。
-
两段构建:当实例被静态构建时,经常缺少构建对象所需的信息,所以,如果将初始化推迟到足够的额外数据时,意味着被构建的对象总是高效的,扁平的。
-
写时复制:当一个对象被复制时,并不复制它的动态成员变量,而是让两个实例共享动态变量,只有在其中某个实例要修改该变量时,才会真正进行复制。
-
批量处理的几个例子(目标:收集多份工作,一起处理他们) :
1、缓存输出:直至缓存满了或是程序遇到行尾符或是文件末尾符。
2、将一个未排序数组转换为堆的最优方法是一次性构建整个堆,开销只有O(n)。
3、多线程的任务队列
4、 在后台保存或更新是使用批量处理的一个例子。
- 高速缓存的几个例子:
1、string会缓存字符串的长度,不会在每次需要时进行计算。
2、线程池缓存了那些创建开销很大的线程。
3、动态规划算法。
- 双重检查: 首先用一种开销不大的检查来排除部分可能性,然后在必要时再使用一个开销很大的检查来测试剩余的可能性。
第六章- 优化动态分配内存的变量
- 从循环处理中或是会被频繁调用的函数中移除对内存管理器的调用,就能显著的改善性能。
- 我们在对一些函数打印信息的时候,在头尾打印信息的时候,应该注意,这个函数,是否是一个线程,是否会不断的运行,如果会不断的运行,那么就不要将信息在线程里面打印,而在线程的头尾打印就好了,以及时间都是这样,线程中的时间如果不好测量,就不要打印。
总结
待更!欢迎收藏此文章~