测量和实验室所有改善程序性能尝试的基础:分析器和计时器软件。
厂商通常会在编译器中提供的分析器profiler。分析器会生成各个函数在程序运行过程中被调用的累积时间的表格报表。对性能优化而言,它是一个非常关键的工具,因为它会列出程序中最热点的函数。第二个工具是计时器软件software timer。开发人员依然可以测量长时间运行的活动来进行性能试验。计时器软件还可以用于测量不受计算限制的任务。第三个工具是非常古老的实验笔记本,许多开发人员认为它已经完全过时了。但是实验笔记本或是其他文本文件仍然是不可或缺的优化工具。
3.1 优化思想
3.1.1 必须测量性能
只有测量才能告诉你到底是在优化游戏中取胜了还是失败了。
他们做出的预测都是可测试的,而且他们会记录下预测;
他们保留代码变更记录;
他们使用可以使用的最优秀的工具进行测量;
他们会保留实验结果的详细笔记;
3.1.2 优化器是王牌猎人
优化器王牌猎人。如果只能让程序的运行速度提高1%是不值得冒险去修改代码的,因为修改代码会引入bug。当性能提升20%的时候,事情就完全不同了。
3.1.3 90/10规则
90/10规则表示某些代码块是被频繁地执行的热点hot spot,这些热点就是我们要进行性能优化的对象。
3.1.4 阿姆达定律
阿姆达定律告诉我们,如果被优化的代码在程序整体运行时间中所占的比例不大,那么即使对它的优化非常成功也是不值得的。
3.2 进行实验
在开始性能调优之前,必须要有正确的代码。需要擦亮眼睛审视这些代码,然后问自己:“为什么这些代码是热点”为什么某个函数与程序中的上百个函数不同,出现了分析器的最差性能列中的最前面?是这个函数浪费了很多时间在冗余处理上吗?有其他更快的方法进行相同的计算吗?这个函数使用了紧缺的计算机资源吗?是这个函数自身已经非常快了,只不过它被调用太多次了,已经没有优化的余地了吗?
实验并不需要证明任何事情。修改后的代码可能会因为某些原因运行得更快或者更慢,但这些原因却与你修改的部分没有任何关系。比如:
当你在测量运行时间时,计算机可能在接收邮件或是检查Java是否有版本更新;
在你重编译之前,一位同事刚刚签入了一个性能改善后的库;
你的修改可能运行得更快,但是处理逻辑却是不正确的。
3.2.1 记实验笔记
优秀的优化人员都会关心可重复性。如果每次的测试运行情况都被记录在案,那么就可以快速地重复实验。
3.2.2 测量基本性能并设定目标
优化工作受两个数值主导,优化前的性能基准测量值和性能目标值。测量性能基准不仅对于衡量每次独立的改善是否成功非常重要,而且对于向其他利益相关人员就优化成本开销做出的解释也是非常重要的。
而优化目标之所以重要,是因为在优化过程中国优化效果会逐渐变小。在优化过程的最初阶段,树上总是有些容易摘取的挂的很低的水果。
从为这些项目设定性能目标开始,这其中有足够多的与用户体验相关的数字,可以让你意识到危险性。
启动时间:
从用户按下回车键至程序进入主输入处理循环所经过的时间。
退出时间:
从用户点击关闭图标或是输入退出命令直至程序实际完全退出所经过的时间。
响应时间:
执行一个命令的平均时间或最长时间。响应时间通常可用粗略地以10的幂为单位划分为以下几个级别:
低于0.1秒:用户在直接控制;
0.1秒至1秒:用户在控制命令;
1秒到10秒:计算机在控制;
高于10秒:喝杯咖啡休息以下;
吞吐量:
吞吐量表述为在一定的测试负载下,系统在每个时间单位内所执行的操作的平均数。
3.2.3 你只能改善你能够测量的
优化一个函数、子系统、任务或是测试用例永远不等于改善整个程序的性能。
3.3 分析程序执行
分析器是一个可以生成另一个程序的执行时间的统计结果的程序。分析器可以输出一份包含每个语句或函数的执行频度,每个函数的累积执行时间的报表。
一种同时支持Windows和Linux的方法如下:
(1)程序员设置一个特殊的可分析程序中所有函数的编译选项,重新编译一次程序,让程序变为可分析的状态。这涉及在每个函数的开始和结束处添加一些额外的汇编语言指令。
(2)程序员可将分析的程序链接到分析库上。
(3)每次这个分析的程序运行时都会在磁盘上生成一张分析表。
(4)分析器读取分析表,然后生成一系列可阅读的文字或图形报告。
(5)通过将优化前的的程序链接至分析库上使其变为可分析状态。分析库中的例程会以非常高的频率中断程序的执行,记录指令指针的值。
(6)每次可分析的程序运行时都会在磁盘上生成一张分析表。
(7)分析器读取分析表,然后生成一系列可阅读的文字或图形报告。
分析器的输出结果可能会有多种形式,一种形式是一份标记有每行代码的执行次数的源代码清单。另一种形式是一份由函数名和该函数被调用的次数组成的清单。第三种形式同样也是函数清单,不过里面记录的是每个函数的累计执行时间和在每个函数中进行的函数调用。还有一种形式是每一份函数和在每个函数中花费的总时间的清单,但不包括调用其他函数的时间,调用系统代码的时间和等待事件的时间。
分析器的分析功能都是量身设计的,它自身的性能开销非常小,程序中每个操作的执行速度只会被下降几个百分点。第一种方法的分析结果会非常精确,代价是更高的间接成本和禁用某些优化。第二种办法是测量的结果是近似值,而且可能会遗漏一些非频繁地被调用的函数,但是它的优点是可以直接运行在正式产品之上。
分析器的最大优点是它直接显示出了代码中最热点的函数,优化过程被简化为列出需要调查的函数的清单,确认各个函数优化的可能性,修改代码,然后重新运行代码得到一份新的分析结果。如此反复,直至没有特别热点的函数或是你无能为力为止。
以本书作者个人分析经验来看,对debug build的分析结果和对release build的分析结果是一致的。在某种意义上,debug build更易于分析,因为其中包含所有的函数,包括内联函数,而正式构建则会隐藏这些被频繁调用的内联函数。
使用分析器是一种帮助我们找到要优化的代码的非常好的方式,但也有它的问题:
分析器无法告诉你更高效的算法可以解决当前的计算性能问题。去优化一个低效的算法只是浪费时间。
对于会执行许多不同任务的待优化的程序,分析器无法给出明确的结果。要想容易地找出最热点的函数,请尽量一次仅优化一个任务。
当遇到IO密集型程序或是多线程程序时,分析器的结果中可能会有误导信息,因为分析器减去了系统的调用的时间和等待事件的时间。
3.4 测量长时间运行的代码
开发人员通过不断地缩小长时间运行的任务的范围直至定位其中一段代码花费了太长时间,感觉不对劲这种方式来查找代码中的热点。
3.4.1 一点关于测量时间的知识
一次完美的测量是指精确地得到大小,重量或者在本书中某个事件每次持续的时间。
3.4.2 用计算机测量时间
1. 硬件时标计数器的发展
time(), clock(), gettimeofday()函数。
如果在全局范围内有许多类会被初始化,这可能会带来问题、当在大型程序
3.4.5 使用测试套件测量热点函数
一旦通过分析器或者运行时分析找出了一个候选的待优化函数,一种简单的改善它的方式是构建一个测试套件,在其中多次调用该函数。这样可以将该函数的运行时间增大为一个可测量的值,同时还可以抵销因后台任务,上下文切换等对运行时间造成的影响。
迭代次数需要凭经验估计。如果使用的时标计数器的有效分辨率是大约10毫秒,那么测试套件在桌面处理器上的运行时间应当是在几百到几千个毫秒。
3.5 评估代码开销来找出热点代码
经验告诉我分析代码和测量运行时间是帮助找出需要优化的代码的两种有效方法。分析器会指出某个函数被频繁的调用或是在程序总运行时间中所占的比率很大。但它不太可能指出某个具体的C++语句可以优化。
开发人员下一步需要做的是,对指出的代码块中的每条语句的开销进行评估。大多数情况下,只需大致观察一下这些语句就能得到它们的开销,然后从中找出性能开销大的语句和语法结构。
3.5.1 评估独立的C++语句的开销
有一条有效的规则能够帮助我们评估一条C++语句的开销有多大,那就是计算该语句对内存读写的次数。
编译器可能会在优化时通过复用之前的计算或是发挥代码静态分析的优势来省略一些内存访问。
3.5.2 评估循环的开销
由于每条C++语句都只会进行几次内存访问,通常情况下热点代码都不会是一条单独的语句,除非受其他因素的作用,让其频繁地执行。
1. 评估嵌套循环中的循环次数
当一个循环嵌套在另一个循环里面的时候,代码块的循环次数是内层循环的次数乘以外层循环的次数。例如;
for (int i = 0; i < 100; ++i)
for (int j = 0; j < 50; ++j)
{
fiddle(a[i][j]);
}
嵌套循环可能并非一眼就能看出来。如果一个循环调用了一个函数,而这个函数中又包含在另外一个循环中,那么内层循环就是嵌套循环。
内层循环可能被嵌入在标准库函数中,特别是处理字符串或者字符的IO函数。
2. 评估循环次数为变量的循环的开销
不是所有循环中的循环次数都是明确的。
3. 识别出隐式循环
响应事件的程序在最外层都会有一个隐式循环 Windows UI程序。
4. 识别假循环
不是所有的while或者do语句都是循环语句。如果do语句帮助控制流程。这种惯用法也时常被用于将几条语句打包为C风格的宏。
3.6 其他找出热点代码的方法
通过使用分析器或是计时器分析代码,开发人员可以向同事和经理展示他们在性能优化工作中取得的进展。
3.7 小结
必须测量性能;
做出可测试的预测并记录预测;
记录代码修改;
如果每次都记录了实验内容,那么就可以快速地重复实验;
一个程序会花费90%的运行时间去执行10%的代码;
只有正确且精确的测量才是准确的测量;
分辨率不是准确性;
只进行有明显效果的性能改善,开发人员就无需担忧方法论的问题;
计算一条C++语句对内存的读写次数,可以估算处一条C++语句的性能开销;