CSAPP:优化程序性能(一)

来源:互联网 发布:下载软件 英文 编辑:程序博客网 时间:2024/06/05 17:51

编写高效程序需要做到以下几点:

第一,必须选择一组适当的算法和数据结构

第二,必须编写出编译器能够有效优化以转换高效可执行代码的源代码(理解优化编译器的能力和局限性很重要)

程序员必须在实现和维护程序的简单性和运算速度之间做出权衡,几分钟就能编写一个简单的插入程序,而一个高效的排序算法程序可能需要一天或更长时间来实现和优化,

大多数编译器,例如GCC向用户提供了一些对它们所使用的优化的限制,最简单的控制就是指定优化级别,以命令行选项-Og 使用一组基本的优化,或者-O1或者更高

(-O2或-O3),编译器必须很小心地对程序进行安全的优化,消除造成不希望的运行时行为的一些可能的原因,为了理解决定一种程序转换是否安全的难度,看看如下两个过程


内存别名使用

void twiddle1(long *xp, long *yp){    *xp += *yp;    *xp += *yp;}void twiddle2(long *xp, long *yp){    *xp += 2 * *yp;}

两个过程似乎有相同的行为,函数twiddle2的效率更高一些,因为它只需要3次内存引用,而twiddle1需要6次内存引用,不过考虑*xp等于*yp的情况,twiddle1的结果是*xp增加了4倍,而twiddle2的结果是*xp的值增加了3倍,编译器不知道函数会被如何调用,因为必须假设参数xp和yp必须相等,因此不可能产生twiddle2作为twiddle1的优化版本(两个指针指向内存同一位置的情况称为“内存别名使用”)。

x = 1000;y = 3000;*q = y;*p = x;t1 = *q;
t1的值取决于p和q是否指向内存的同一位置,如果不是则t1等于3000,如果是的话则t1等于1000。这造成了一个主要的妨碍优化因素,严重限制了编译器优化策略。


函数调用

long f();long func1(){    return f() + f() + f() +f();}long func2(){    return 4 *f();}

最初看上去两个过程结果相同,但是考虑下面f代码

long counter = 0;long f(){    return counter++;}

函数有副作用——它修改了全局变量的一部分,改变调用次数会改变程序的行为,大多数编译器都不会考虑程序是否具有副作用,它会假设最糟的情况,并保持所有的函数调用不变。


内联函数替换

包含函数调用的代码可以使用内联函数替换来进行优化,将函数调用替换为函数体

将func1替换如下:

long func1in(){    long t = counter++;    t += counter++;    t += counter++;    t += counter++;    return t;}
这样既减少了函数调用开销也可以对代码进一步优化:

long func1opt(){    long t = 4 * counter +6;    counter += 4;    return t;}
GCC的最近版本会尝试这种优化,并且只尝试在单个文件中定义函数的内联,这意味着它无法应用于常见的情况——文件之间的函数调用。

就优化能力来说GCC是胜任的,但是它不会做那种激进变换的优化。


表示程序性能

引入度量标准每元素的周期数(Cycles Per Element,CPE)来表示程序性能并指导改进代码

处理器活动顺序由时钟控制,时钟提供某个频率的规律信号,通常用GHz(千兆赫兹)即十亿周期每秒来表示,例如一个处理器是4GHz,这表示处理器时钟的运行频率为每秒4e9个周期,这里要强调一点:

在中国,1兆 = 1e12 也就是1万亿

在西方,1兆 = 1e6 也就是1百万 所以千兆就是1e9

每个周期的时间就是时钟频率的倒数,也就是1e-9秒(1纳秒)

计算长度为n的向量的前置和,对于向量a=<a0, a1 , ... , an-1>,前置和向量p=<p0, p1, ..., pn-1>定义为

p0 = a0

pi = pi-1 + ai, 1<= i < n

void psum1 (float a[], float p[], long n){    long i;    p[0] = a[0];    for(i = 1, i < n, i++)        p[i] = p[i-1] + a[i];}void psum2(float a[], float p[], long n){    long i;    p[0] = a[0];    for( i=1; i < n-1; i +=2){        float mid_value = p[i-1] + a[i];        p[i] = mid_value;        p[i+1] = mid_vlaue + a[i+1];    }    if (i < n)        p[i] = p[i-1] + a[i];}
psum1每次迭代计算结果向量的一个元素

psum2函数使用循环展开,每次迭代计算两个元素

这个过程所需的时间可以使用一个常数加上一个与被处理元素个数成正比的因子来描述(应该就是二元一次方程吧),使用最小二乘拟合,我们发现psum1和psum2的运行时间(以时钟周期为单位)分别接近于368 + 9.0n 和 368 + 6.0n,代码计时和初始化过程、准备循环以及完成过程的开销为368个周期,再加上每个元素6.0或9.0周期的线性因子,这些项中的系数成为每元素的周期数(简称CPE),根据这种度量标准,psum2的CPE为6.0,psum1的CPE为9.0。