本周继续。
如何让程序跑的更快,是每一个做程序员的职责。让程序跑的快的好处这里就不阐述了,编写搞笑的程序需要几类活动:
1、选择高效的算法和数据结构
2、能够写出编译器有效优化转换成可执行代码的源代码(这需要理解编译器的能力和局限性)
3、针对处理运算量特别大的计算,需要将一个任务分成多部分
编译器的能力和局限性
void twiddle1(int *xp, int *yp)
{
*xp += *yp;
*xp += *yp;
}
void twiddle2(int *xp, int *yp)
{
*xp += 2* *yp;
}
乍一看,这两个函数的功能是一样的。 而且twiddle2 的效率更好一点,它只要求3次存储器的引用,而twiddle1却要6次存储器的引用。
不过考虑 xp 等于yp的情况下,teiddle1会执行下面的结果:
*xp += *xp; *xp += *xp; 结果是xp的结果会增加4倍,
而teiddle2来说
*xp += 2* *xp; 这时xp的结果是增加3倍的
编译器不知道twiddle1何时被调用,因此它必须假设参数xp和yp可能会相等, 因此它是不能产生teiddle2这样的优化版本的。
第二个妨碍优化的因素是函数调用:
int f();
int func1(){ return f() + f() +f() + f(); }
int func2(){ reutrn 4*f();}
最初看上去func1 和func2 计算的过程是相同的,当以func1为源时,很容易想象func2的优化
不过,考虑一下代码
int count = 0;
int f() {
return count++;
}
显然,这个函数是有严重的副作用的,大多数编译器不会试图判断一个函数有没有副作用,但是编译器会假设最糟糕的情况,并保持所有的函数调用不变。
所以,大多数情况下,我们不能太依赖于编译器的优化能力,需要自己写出能适应编译器高效优化的代码。
看下面的例子
typedef int data_t ;
#define OP +
#define OP * //这里把它理解为两个不同的操作,待会是介绍
typedef struct{
long int len;
data_t *data;
}vec_rec, *vec_ptr;
vec_ptr new_vec(long int len)
{
vec_ptr = result = (vec_ptr) malloc(sizeof(struct vec_rec));
if(!result) { return NULL;}
result->len = len;
if(len >0)
{
data_t *data = (data_t*) calloc (len, sizeof(struct vec_rec));
if(!data){ free((void *)result); return NULL;}
result->data = data;
}
else
{ result->data = NULL; }
return result;
}
int get_vec_element(vec_ptr v , long int index, data_t *dest)
{
if( index<0 || index >= v->len)
return 0;
*dest = v->data[index];
return 1;
}
long int vec_length(cec_ptr v)
{ return v->len; }
void combine1(vec_ptr v , data_t *dest)
{
long int i;
*dest = INDEX;
for(int i=0; i<vec_length(v); i++)
{
data_t val;
get_vec_element(v, i , &val);
*dest = *dest OP val;
}
}
在实际程序中, 数据类型data_t 被声明为int、float 和double
对于程序的性能,我们使用度量标准 每元素的周期数作为性能的衡量(数字越小 性能越好)
对于上面的程序
函数 方法 整数 (+ *)
combine1 抽象的未优化 29.02 29.21
combine1 抽象的-O1 12.00 12.00
从上面可以看到,使用优化的级别1,程序员不需要做任何工作,就可以把系能提高一倍,以后优化我们暂且跟编译器优化级别1 来做比较
消除循环的低效率
可以观察到,combine1 调用函数vec_length作为for循环的测试条件,不仅每一次都会对测试条件求值,而且我们发现这个值是不会变化的,改成下面的代码
void combine2(vec_ptr v , data_t *dest)
{
long int i;
*dest = INDEX;
long int length = vec_length(v);
for(int i=0; i< length; i++)
{
data_t val;
get_vec_element(v, i , &val);
*dest = *dest OP val;
}
}
函数 方法 整数 (+ *)
combine1 抽象的-O1 12.00 12.00
combine2 抽象的未优化 8.03 8.09这个优化的效果是显而易见的,超过了编译器的第一级别的优化,这个优化是一类常见的优化的一个例子,成为代码移动,这列优化包括识别要执行多次但是计算结果不会改变的计算。
减少过程调用
像我们看到的那样,过程调用会带来相当大的开销,而且妨碍大多数形式的程序优化,从combine2中来看,每次循环都会调用get_vec_element来获取下一个向量的元素。
data_t *get_vec_start(vec_ptr)
{ return v->data; }
void combine3(vec_ptr v , data_t *dest)
{
long int i;
*dest = INDEX;
long int length = vec_length(v);
data_t *data = get_vec_start(v);
for(int i=0; i< length; i++)
{
data_t val;
*dest = *dest OP data[i];
}
}
这样做会可能会严重损害程序的模块性,因为这么做没有做 i 的界限判断,这个问题是很严重的,而且看效率函数 方法 整数 (+ *)
combine2 抽象的未优化 8.03 8.09combine3 直接访问数组 6.01 8.01
我们得到的性能的提升出乎意料的普通, 只提高了整数的性能
消除不必要的存储器引用
在循环中,有这样的代码:
*dest = *dest OP data[i];
汇编代码可以清晰的看到 取了两次寄存器中的值,而且循环还在继续,所以改!!!
void combine4(vec_ptr v , data_t *dest)
{
long int i;
*dest = INDEX;
long int length = vec_length(v);
data_t *data = get_vec_start(v);
data_t acc = INDEX;
for(int i=0; i< length; i++)
{
data_t val;
acc = acc OP data[i];
}
*dest =acc;
}
看性能函数 方法 整数 (+ *)
combine3 直接访问数组 6.01 8.01
combine4 累积在临时变量中 2.00 3.00可以看到 性能的提升是显著的
理解现代处理器
现在处理器都是多核并且可以并行处理
void combine5(vec_ptr v , data_t *dest)
{
long int i;
*dest = INDEX;
long int length = vec_length(v);
data_t *data = get_vec_start(v);
data_t acc = INDEX;
for(int i=0; i< length; i+=2)
{
acc = (acc OP data[i] ) OP data[i];
}
for(; i<length; i++)
{
acc = acc OP data[i];
}
*dest =acc;
}
这里的i =2 表示展开两次,
函数 方法 整数 (+ *)
combine3 直接访问数组 6.01 8.01
combine4 累积在临时变量中 2.00 3.00combine5 展开2次 2.00. 1.50
展开3次(i=3) 1.00 1.00
可以看到 当i=3时 性能几乎到达极限
看处理器在一个的特点:处理器的分支预测
int minmax1(int a[], int b[], int n)
{
int i ;
for(i=0; i< n; i++)
{
if(a[i] > b[i])
{
int t = a[i];
a[i] = b[i];
b[i] = t;
}
}
}
对于这个函数的CPE值大约14.50 对于可预测的数据,CPE的值为3.00~4.00,很明显是高于预测错误惩罚的痕迹。
void minmax2(int a[], int b[], int n)
{
int i ;
for( i =0; i<n; i++)
{
int min = a[i]>b[i]?a[i]"b[i];
int max = a[i] <b[i]?a[i]:b[i];
a[i] = min;
b[i] = max;
}
}
对于这个函数的测试,无论数据是任意的,还是可以预测的,CPE的值都大约是5.0.
不是所有的条件行为都是可以预测的,但是一些条件还是可以通过程序来实现的,而且效果会很好。
总结一下, 提升程序性能的一些因素:
高级设计、消除连续函数调用、消除不必要的存储器调用、展开循环、用功能的风格重写代码