编写高效程序需要做到以下几点:
第一,必须选择一组适当的算法和数据结构
第二,必须编写出编译器能够有效优化以转换高效可执行代码的源代码(理解优化编译器的能力和局限性很重要)
程序员必须在实现和维护程序的简单性和运算速度之间做出权衡,几分钟就能编写一个简单的插入程序,而一个高效的排序算法程序可能需要一天或更长时间来实现和优化,
大多数编译器,例如GCC向用户提供了一些对它们所使用的优化的限制,最简单的控制就是指定优化级别,以命令行选项-Og 使用一组基本的优化,或者-O1或者更高
(-O2或-O3),编译器必须很小心地对程序进行安全的优化,消除造成不希望的运行时行为的一些可能的原因,为了理解决定一种程序转换是否安全的难度,看看如下两个过程
内存别名使用
void twiddle1(long *xp, long *yp)
{
*xp += *yp;
*xp += *yp;
}
void twiddle2(long *xp, long *yp)
{
*xp += 2 * *yp;
}
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();
}
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=
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。