CSAPP学习笔记 -- 第五章 优化程序性能
前言
写程序的目标
-
正确工作
-
可读性好
-
运行效率
-
必须选择一组适当的算法和数据结构
-
必须编写出编译器能够有效优化以转换成高效可执行代码的源代码
即使是最好的编译器也受到妨碍优化的因素的阻碍,妨碍优化的因素就是程序行为中那些严重依赖于执行环境的方面。
程序优化
-
第一步就是消除不必要的工作,让代码尽可能有效的执行所期望的任务
-
第二步是利用处理器提供的指令级并行能力,同时执行多条指令
代码剖析程序是测量程序各部分性能的工具,这种分析能够帮助我们找到代码中低效率的地方,并且确定程序中我们应该着重优化的部分。
5.1 优化编译器的能力和局限性
安全的优化:对于程序可能遇到的所有可能的情况,在C语言标准提供的保证之下,优化后得到的程序和未优化的版本有一样的行为。
内存别名使用:两个指针可能指向同一个内存位置。在安全的优化中,编译器必须假设内存别名使用的情况。这会造成一个主要的妨碍优化的因素。
函数副作用:会修改全局程序状态的一部分,导致其调用次数的改变会改变程序行为。
5.2 表示程序性能
每元素的周期数CPE
代表程序性能,指导改进代码
5.3 程序示例
5.4 消除循环的低效率
代码移动:这类优化包括识别要执行多次(例如在循环里)但是计算结果不会改变的计算,因而可以将计算移动到代码前面不会被多次求值的部分,仅计算一次,然后调用计算结果即可。
判断能否进行代码移动需要非常成熟完善的分析,远超出当前编译器的能力,需要程序员自己进行这样的变换。
如果函数有副作用,那么程序员就必须经常显式地帮助编译器来完成代码的移动。
一个看上去无足轻重的代码片段有隐藏的渐近低效率。
5.5 减少过程调用
过程调用会带来开销,而且妨碍大多数形式的程序优化。
5.6 消除不必要的内存引用
引入临时变量来减少内存访问次数
5.7 理解现代处理器
随着试图进一步提高性能,必须考虑利用处理器微体系结构的优化,也就是处理器用来执行指令的底层系统设计。
有两种下界描述了程序的最大性能
-
延迟界限:当一系列操作必须按照严格顺序执行时,就会遇到延迟界限,因为在下一条指令开始前,这条指令必须结束。当代码中的数据相关限制了处理器利用指令级并行的能力时,延迟界限能够限制程序性能。
-
吞吐量界限:刻画了处理器功能单元的原始计算能力,这个界限是程序性能的终极限制。
5.7.1 整体操作
-
超标量:可以在每个时钟周期执行多个操作
-
乱序:执行顺序与机器级程序顺序不一定一致
-
指令控制单元:从内存中读出指令序列,并根据这些指令序列生成一组针对程序数据的基本操作
-
执行单元:执行操作
-
需要更大更复杂的硬件,达到更高的指令集并行度
-
退役单元:记录正在进行的处理,并确保它遵守机器级程序的顺序语义
-
寄存器重命名:控制操作数在执行单元间传送的最常见的机制
5.7.2 功能单元的性能
-
发射时间为1的功能单元被称为完全流水线化的
-
最大吞吐量:发射时间的倒数
-
延迟界限:任何必须按照严格顺序完成合并运算的函数所需要的最小CPE值
-
吞吐量界限:CPE的最小界限
5.7.3 处理器操作的抽象模型
-
从机器级代码到数据流图
-
-
形成循环的代码片段中,寄存器分为四类:只读,只写,局部,循环
-
-
其他性能因素
-
数据流中的关键路径提供的知识程序需要周期数的下界
-
还有其他因素限制性能,包括可用的功能单元数量和任何一部中功能单元之间能够传递数据值的数量
5.8 循环展开
循环展开是一种程序变换,通过增加每次迭代计算的元素的数量,减少循环的迭代次数。
-
首先,他减少了不直接有助于程序结果的操作的数量,例如循环索引计算和条件分支
-
第二,他提供了一些方法,可以进一步变化代码,减少整个计算中关键路径上的操作数量
5.9 提高并行性
5.9.1 多个积累变量
我们可通过将一组合并运算分隔成两个或更多的部分(多路并行),并在最后合并结果提高性能。
既做循环展开也做多路并行可以打破延迟界限设下的性能限制。
5.9.2 重新结合变换
重新结合变换:改变计算的结合顺序。
没有循环展开和并行累积可靠。
-
关键路径上只有n/2个操作,因此最小可能的CPE减少了一半
5.10 优化合并代码的结果小结
我们可能需要非常程式化的方式编写程序将这些提升性能的能力诱发出来
5.11 一些限制因素
5.11.1 寄存器溢出
如果我们的循环并行度p超过了可用的寄存器数量,那么编译器就会诉诸溢出。
一旦编译器必须要诉诸溢出,那么维护多个累积变量的优势很可能消失。
5.11.2 分支预测和预测错误处罚
如果预测正确,处理器就会“提交”投机执行的指令的结果,把他们存储到寄存器或内存。
如果预测失败,处理器必须丢弃所有投机执行的结果,在正确的位置,重新开始取指令的过程。这样做会引起预测错误处罚,在产生有用的结果之前,必须重新填充指令流水线。
分支预测原则
-
不要过分关心可预测的分支
-
书写适合用条件传送实现的代码
-
GCC能够以一种更“功能性”的风格书写的代码产生条件传送
5.12 理解内存性能
5.12.1 加载的性能
一个包含加载操作的程序的性能既依赖于流水线的能力,也依赖于加载单元的延迟。
5.12.2 存储的性能
写/读相关
5.13 应用:性能提高技术
一些优化性能的基本策略
-
高级设计:选择适当的算法和数据结构
-
基本编码原则:避免限制优化的因素
-
消除连续的函数调用
-
消除不必要的内存引用
-
低级优化
-
展开循环,降低开销,并且使得进一步的优化成为可能
-
通过使用例如多个累积变量和重新结合技术,提高指令级并行
-
用功能性的风格重写条件操作,使得编译采用条件数据传送
忠告:要警惕,在为了提高效率重写程序时避免引入错误。
5.14 确认和消除性能瓶颈
代码剖析程序
系统优化的通用原则:Amdahl定律
5.14.1 程序剖析
剖析报告
-
执行各个函数花费的时间
-
函数的调用历史
5.14.2 使用剖析程序来指导优化
这一小节需要仔细研究并理解书上示例。