优化程序性能
首先要清楚功能和性能之分:功能是软件系统能为用户做什么,满足什么样的需求,重点在
于做什么。而性能是衡量软件好坏的重要因素,重点在于做的怎样。
如何编写高效程序可通过以下三点:
1.
算法和数据结构
2.
编译器可有效优化的源代码//完全依赖于程序员的水平
3.
多线程并行运算
本章着重讲的就是编译器做了些什么和如何让编译器更好的工作即第二点。
要了解到编译器本身的优化能力很强,但是并不是万能的,它可能导致与程序员意图不一的
结果。所以编译器采取的优化处理是很保守的,因为正确性是第一位的!
编译器的五种级别:
1.
-O0 关闭所有优化等级
2.
-O1 基础优化
3.
-O2 试图提高代码性能而不会增大体积和大量占用的编译时间
4.
-O3 最高级,有时性能不增反降,不推荐使用
5.
-Os 优化代码尺寸(-O2.5)
存储器的局限性:
1.
存储器别名使用 xp=yp
2.
函数调用:调用四次同名函数
所以虽然编译器的优化能力很强,但也有一定的局限性,过于依赖编译器会带来意想不到的错误,我们应该努力写出更适合优化的代码,观察在不同级别优化之后的结果,可以提高我们优化代码的能力。
了解完了编译器能做的工作和他的局限性之后,来认识一下一个用来衡量程序性能的指标:CPE。
表示程序性能:CPE
CPE(cycles per element):每元素的周期数,执行一个元素需要多少个时钟周期。即一个元素的运行时间。一个时钟周期为时钟频率的倒数。
程序的优化等级是如何划分和实现的呢?
程序的优化过程:combine1函数:对于已知的数组v,循环遍历其所有元素,进行累加或累乘。将结果赋值给*dest
消除循环的低效率:combine2函数:代码移动,即识别多次执行但值不变的代码,将其移到代码前部分,避免重复求值。
减少函数调用:combine3:消除循环中的函数调用
消除不必要的存储器引用:定义一个局部变量,使其在循环中累计值,在循环结束后再将值写入内存。从而消除不必要的存储器引用。
使用局部变量:combine4函数。
如果要进一步了解优化,就必须对计算机体系结构与处理器的知识有所了解。
计算机由五大部件组成
控制器:分析和执行机器指令并控制各部件的协同工作
运算器:根据控制信号对数据进行算术和逻辑运算
存储器:内存存储中间结果,外存存储需要长期保存的信息
输入设备:输入数据和程序
输出设备:输出处理结果
冯诺依曼机的特点:
1.
计算机由五大部件组成
2.
指令和数据以同等地位存于存储器,可按地址寻访
3.
指令和数据用二进制表示
4.
指令由操作码和地址码组成
5.
存储程序
6.
以运算器为中心
理解现代处理器:
超标量处理器:
可在一个时钟周期内发出和执行多条指令。指令从顺序指令流中检索,通常是动态调度的。(乱序执行)
指令周期是取出并执行一条指令的全部时间,包括取址阶段和执行阶段,在取址阶段进行取址和分析,在执行阶段执行指令。
取指令:指令由主从取到指令寄存器。每取一条指令,pc自加一。
指令译码:对指令进行拆分和解释
执行:完成指令所规定的各种操作
流水线技术:
取指令IF:
指令译码ID
取操作数OF
执行EX
写回WB
分析combine4:现代处理器操作是同时对多条指令求值。多条指令并行执行。分析,整理数据流图,分析关键路径
出现(%rax,%rdx,4)一般就是数组了。
Load mul add cmp jg 与其相关的寄存器连接起来。
循环展开:真正意义上的优化(combine5)
将for循环展开两次,每次循环处理处理两个元素,将limit限定为length-1,防止越界
加入一个新的for循环,处理limit以外的元素
延迟界限:当一系列操作必须按照严格的顺序执行,处理每个元素所经历的关键路径的周期数。
吞吐量界限:刻画了处理器功能单元全力运行时的原始计算能力。
提高并行性:多个累积变量(combine6)
循环1的代码中加入两个累积变量acc0和acc1.
两次循环展开,两路并行
combine5: acc = (acc OP data[i]) OP data[i+1]转变为 combine6: acc0 = acc0 OP data[i] acc1 = acc1 OP data[i+1];
提高并行性:重新结合变换(combine7)
combine5 acc = (acc OP data[i]) OP data[i+1] combine7 acc = acc OP (data[i] OP data[i+1])
乘法运算在combine7中得到显著提高。
重新结合变换有时能减少计算中关键路径的数量,但并不是总是有效的方法。相比之下,k次循环展开和k路并行累积更可靠。
进行优化的限制因素:
寄存器溢出,只有少量寄存器可以存放累积值当并行度超过了可用寄存器的数量,编译器就会将结果溢出到栈中,性能就会急剧下降,因为访问存储器的时间会长的多。
避免分支预测和预测错误处罚。
从combine1到combine7提高了至少10倍的效率。循环展开,并行累积值是可靠的提高
程序性能的方法。当处理的数据是小于1000个元素的向量,数据量不会超过8000个字节,
这时可用充分利用高速缓存cache。
理解存储器性能
加载:从存储器读到cpu寄存器,受存储操作结果的影响,数据相关。
1.
流水线的能力
2.
加载单元操作的延迟
通过这两点影响性能
存储:从寄存器写入存储器:理论上不产生数据相关
写读相关:一个存储器读的结果依赖于一个最近的存储器的写
存储单元多了一个存储缓冲区,一系列的存储操作不必等待 每个操作都更新高速缓存就能够开始执行
当加载操作发生的时候,加载单元必须先检查存储缓冲区, 看看有没有相匹配的条目,如果匹配成功就从存储缓冲区中 取出数据作为加载的结果
内循环数据流
对于寄存器操作,在指令译成操作的时候,处理器就可以确定哪些指令会影响其他哪些指令
对于存储器操作。只有到加载和存储的地址被计算出来以后,处理器才能确定哪些指令会影响其他的哪些指令
优化程序总结
1.
高级程序语言设计:选择合适的算法和数据结构
2.
基本编码规则:消除连续函数调用;消除不必要的存储器引用,引入临时变量保存中间值
3.
低级优化:循环展开,降低开销;使用多个累积变量或者重新结合变换,提高指令级并行,编程k路并行程序,用功能的风格重写条件操作,使得编译采用条件数据传送。
4.
减少相关性
Amdahl定律:加速比s=1/((1-a)+a/k);a为该部件的重要性,k为该部件速度提高了多少倍
怎样获得更有效/快的程序:
消除函数调用,条件测试,存储器引用
从处理/存储器角度循环展开,提高并行性,减少相关性
CPE,分析数据流图,找关键路径,找到最耗时部分
补充:指令间的相关性
1.
结构相关
2.
数据相关:某条指令的操作数依赖于前一条或前几条指令的运行结果
3.
控制相关
超标量技术:处理器中含有多少流水线
每个时钟周期能够译码,发射,执行多条指令
乱序执行:提高指令流执行效率,减少数据相关,控制相关