导言
- 编写高效程序:适当的算法和数据结构;源代码能被编译器优化为高效可执行的代码;运算量特别大的计算,将一个任务分成多个部分,这些部分可以在多核和多处理器的某种组合上并行地计算。
- 许多低级别优化:降低可读性、模块性=>易出错,难修改、维护=>要维护代码一定程度的简洁和可读性。
- 妨碍优化:严重依赖于执行环境的程序行为
- 程序优化:消除不必要工作(消除不必要的函数调用、条件测试、内存引用);利用处理器提供的指令级并行能力,同时执行多条指令。(降低一个计算不同部分之间的数据相关,增加并行度)
- 代码剖析程序:测量程序各个部分性能(帮助找到代码中低效率的地方=>应该着重优化的部分)
- 试错法实验
- 研究汇编代码(理解编译器、代码运行)、内循环(降低性能的属性:过多的内存引用、寄存器使用不当汇编代码(预测将被执行的操作、处理器资源的使用)
- 关键路径:是在循环的反复执行过程中形成的数据相关链。通过确认关键路径来决定执行一个循环需要的时间/至少是一个时间下界。
- 只重写程序到编译器由此就能产生有效代码所需要的程度=>尽量避免损害代码的可读性、模块性和可移植性。
- 不断修改源代码,试图欺骗编译器产生有效的代码。
5.1 优化编译器的能力和局限性
- 最简单的对编译器所使用的优化的控制是指定优化级别。
-Og:基本优化;-O1或更高(-O2或-O3):大量优化。=>程序性能提高,程序规模提高,使标准的调试工具更难对程序进行调试。
主要考虑以优化级别-O1编译出的代码。
可以写出的c代码,即使用-O1选项编译得到的性能,也比用可能的最高的优化等级编译一个更原始的版本得到的性能好。直接优化代码本身(对c代码进行调整)可能会比 用高级优化等级编译 的运行性能要更好。
- 编译器会确保优化后得到的程序和未优化的版本有一样的行为。
- 优化障碍:
内存别名使用:两指针指向同一内存单元。
边界检查:在使用一个变量前,用来检查该变量是否处在一个特定范围之内的过程(如数组下标检查:防止下标超出数组范围而覆盖其他的数据):存在不可能越界的代码
函数调用:编译器会假设最糟的情况,并保持所有的函数调用不变。(如:函数调用过程中修改了全局程序状态)=>用内联函数替换来优化函数调用(包含函数调用的代码可以用一个称为内联函数替换的过程进行优化,将函数调用替换为函数体(GCC只尝试在单个文件中定义的函数的内联))
-
GCC完成基本的优化。
5.2 表示程序性能
- CPE(每元素的周期数):度量标准,表示程序性能并指导改进代码的方法。
- 用时钟周期来表示,度量值表示的是执行了多少条指令,而不是时钟运行的有多快。
- 一个过程所需要的时间可以用:常数+与被处理元素个数成正比的因子*被处理元素个数 来描述。
5.3 程序示例
边界检查降低了程序出错的机会,但是它也会减缓程序的执行。
5.4 消除循环低效率
代码移动
隐藏的渐近低效率
5.5 减少过程调用
5.6 消除不必要的内存引用
每次迭代时,累积变量的数值都要从内存读出再写入到内存。为消除这种不必要的内存独写,引入一个临时变量,在循环中用来累积计算出来的值。
笔记:
一般有用的优化:
- 移动代码到循环外/提前计算
- 减少乘=>加/移位
- 公共因子统一处理
- 计算临时量,保存中间结果
优化障碍:
- 过程调用
- 内存别名使用