第五章 优化程序性能

 

写程序的最主要目标就是使它在所有可能的情况下都正确工作。

程序员必须写出“清晰简洁”的代码,读懂、理解、修改   代码。

       编写高效程序

1.          选择合适的算法和数据结构

2.          编写出编译器能够有效优化以转换成高效可执行代码的源代码。

3.          针对运算量特别大的计算,并行计算(将一个任务分成多个部分,这些部分可以在多核和多处理器的某种组合上并行地计算)。

 

程序优化:

l  第一步:消除不必要的内容,让代码尽可能有效地执行它期望的工作。这包括不必要的函数调用、条件测试和存储器引用。(5.4~5.6,不依赖于目标机器的任何特性)

l  第二步:利用处理器提供的指令级并行能力,同时执行多条指令。(5.7~5.9,利用处理器微体系结构    的优化)

 

代码剖析程序(profiler):

Amdahl定律:

研究程序的汇编代码表示,是理解编译器,以及产生的代码如何运行的最有效的手段之一。

 

 

5.1、优化编译器的能力和局限性

存储器别名使用(memory aliasing):两个指针可能指向同一个存储器位置的情况。

副作用:改变函数调用的次数会改变程序的行为。(修改了全局程序状态的一部分)。

 

妨碍优化的因素:

       ① 如果编译器不能确定两个指针是否指向同一个位置,就必须假设什么情况都有可能,限制了可能的优化策略。

    ② 函数调用。大多数编译器不会试图判断一个函数是否没有副作用,因此任意函数都可能是优化的候选者。编译器会假设最糟的情况,并保持所有的函数调用不变。

 

       用内联函数替换优化函数调用:将函数调用替换为函数体,既减少了函数调用的开销,也允许对展开的代码做进一步优化。

       就优化能力来说,GCC被认为是胜任的,但是并不是特别突出,它完成了基本优化。

 

5.2、表示程序性能

每元素的周期数(CyclesPer Element,CPE):表示程序性能并指导我们改进代码的方法。

这样的度量标准对执行重复计算的程序来说是很合适的。

处理器活动的顺序是由时钟控制的。用时钟周期来表示,度量值表示的是执行了多少条指令。

5.3、程序示例

 

5.4、消除循环的低效率

       代码移动(code motion):这类优化包括识别要执行多次(例如在循环里)但是计算结果不会改变的计算。

程序员必须经常帮助编译器显式地完成代码移动。

       库函数strlen 的调用:

C语言中,字符串是以null 结尾的字符序列,strlen必须一步一步的检查这个序列,知道遇到null字符。对于一个长度为n的字符串,strlen所用的时间与n成正比。

size_t strlen(const char *s)

{

size_t length =0 ;

       while(*s != ‘\0’ )

              {

                     s++;

                     length++;

              }

       returnlength;

}

 

       一个看上去无足轻重的代码片段有隐藏的渐近低效率(asymptotic inefficiency)。对于一个100万个字符的字符串,这段无危险的代码变成了一个主要的性能瓶颈。

一个有经验的程序员工作的一部分就是避免引入这样的渐近低效率。

5.5、减少过程调用

过程调用会带来相当大的开销,而且妨碍大多数形式的程序优化。

消除循环中的函数调用,得到的代码运行速度快很多,这是以损害一些程序的模块性为代价的。

5.6、消除不必要的存储器引用

       在临时变量中存放结果,消除了每次循环迭代中从存储器中读出并将更新值写回的需要。(p355中例子)

 

5.7、理解现代处理器

       试图进一步提高性能,必须考虑利用处理器微体系结构的优化,也就是处理器用来执行指令的底层系统设计。

       在实际的处理器中,是同时对多条指令求值,这个现象称为“指令级并行”。

多条指令可以并行地执行,同时又呈现一种简单的顺序执行指令的表象。

 

5.8、循环展开

循环展开:通过增加每次迭代计算的元素的数量,减少循环的迭代次数。

循环展开k次,上限设为(n-k+1),在循环内对元素i到i+k-1 应用合并运算,每次迭代,循环索引i加k。再处理后面的几个元素。

循环展开对浮点运算没有帮助,但是对整数加法和乘法有用。

5.9、提高并行性

(1)多个累积变量

将一组合并运算分割成两个或更多的部分,并在最后合并结果来提高性能。

补码运算是可交换和可结合的,甚至是当溢出时也是如此。整数运算是可结合的。

浮点乘法和加法不是可结合的,由于四舍五入和溢出,可能产生不同的结果。

(2)重新结合变换

括号改变合并顺序。

 

循环展开和并行地累积在多个值中,是提高程序性能的更可靠的方法。

5.10、优化合并代码的结果小结

 

 

5.11、一些限制

(1)寄存器溢出

循环并行性的好处受到描述计算的汇编代码的能力限制。如果并行度P超过了可用的寄存器数量,那么编译器会诉诸溢出(spolling),将某些临时值存放到中。一旦出现这种情况,性能会急剧下降。

(2)分支预测和预测错误处罚

C程序员怎么能够保证分支预测处罚不会阻碍程序的效率呢?两个通用原则:

Ø  不要过分关心可预测的分支

Ø  书写适合用条件传送实现的代码

5.12理解存储器性能

所有的现代处理器都包含一个或多个高速缓存存储器(cache),以对这样少量的存储器提供快速的访问。

如何编写充分利用高速缓存的代码,来提高程序性能。

(1)       加载

加载:从存储器读到寄存器

(2)       存储

存储:从寄存器写到存储器

5.13、应用:性能提高技术

       优化程序性能的基本策略:

1)       高级设计:选择适当的算法和数据结构。

2)       基本编码原则:避免限制优化的因素,编译器就能产生高效的代码。

l  消除连续的函数调用。在可能时,将计算移到循环外。考虑有选择地妥协程序的模块性以获得更大的效率。

l  消除不必要的存储器引用。引入临时变量来保存中间结果。只有在最后的值计算出来时,才将结果存放到数组或全局变量中。

3)       低级优化

l  展开循环,降低开销。

I 通过使用例如多个累积变量和重新结合等技术,找到方法提高指令级并行。

l  用功能的风格重写条件操作,使得编译采用条件数据传送。

 

5.14、确认和消除性能瓶颈

(1)程序剖析

程序剖析(profiling):包括运行程序的一个版本,其中插入了工具代码,以确定程序的各个部分需要多少时间。

代码剖析程序(codeprofiler)是在程序执行时收集性能数据的分析工具。

       Unix系统提供一个剖析程序GPROF。产生两种形式的信息:

²  确定每个函数花费了多少CPU时间。

²  计算每个函数被调用的次数,以执行调用的函数来分类。

剖析报告:

第一部分是执行各个函数花费的时间,按照降序排列。

第二部分是函数的调用历史。

GPROF属性:

l  记时不是很准确。

l  调用信息相当可靠

l  默认情况下,不会显示对库函数的调用。

 

(3)    用 剖析程序 来指导优化

 

(3)Amdahl 定律

主要思想:当我们加快系统一个部分的速度时,对系统整体性能的影响依赖于这个部分有多重要和速度提高了多少。

加速比:

Amdahl定律的主要观点---要想大幅度提高整个系统的速度,我们必须提高整个系统很大一部分的速度。

5.15、小结(重要)

猜你喜欢

转载自blog.csdn.net/weixin_41413441/article/details/79433203