您的位置:首页 > 其它

优化程序性能

2015-07-31 00:29 225 查看
程序员需要在实现和维护程序的简单性和程序运行速度之间做权衡。

编写高效程序三种手段:

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

2、编写编译器能有效优化的代码。

3、并发编程:一个任务分成多个,用不同处理器分别完成。

程序优化步骤:

1、消除不必要的内容,消除不必要的函数调用、条件测试和存储器引用。

2、利用处理器提供的指令集并行能力,同时执行多条指令。

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

一般情况下,编译器会利用一些机会来简化表达式。简化的程度我们用优化级别表示,GCC中-O1、-O2、-O3的优化级别依次递增。

注意:编译器对程序只用“安全的”优化,即优化的程序和未优化的版本有一致的行为。

但是,有一些因素会阻碍编译器的优化。

a)、存储器别名使用

考虑如下两个函数:

void twiddle1(int *xp, int *yp)
{
<span style="white-space:pre">	</span>*xp +=*yp;
<span style="white-space:pre">	</span>*xp +=*yp;
}

void twiddle2(int *xp, int *yp)
{
<span style="white-space:pre">	</span>*xp += 2* *yp;
}
这两个函数看似完成了相同的功能,但其中twiddle1使用了6次存储器调用,而twiddle2只用了3次。因此,照理来说编译器应该把twiddle1函数优化为twillde2函数的行为。但是,如果考虑xp与yp相等的情况(称为存储器别名使用),此时最终*xp的值就不等了。而编译器不能确定两个指针是否指向同一个位置,也就是说其必须假设什么情况都会方式,这就限制了其优化策略。

b)、函数调用

举例:

int f();
int func1()
{
return f() + f() + f() + f();
}
int func2()
{
return 4*f();
}
这两个函数看上去结果相同,但func2()只调用f()一次,而func1()调用f()4次,因此看起来编译器应该将func1()优化成func2()的形式。

但是,这两个函数实际上在一些特殊情况下的行为是不一致的。考虑下面f()代码:

int counter = 0;
int f()
{
return counter++;
}
通过调用func1()和func2()后所得结果分别为6和0,这是由于f()会修改全局变量引起的。

由于编译器无法确定f()函数是否有副作用,因此编译器会保持所以函数最初的状态,即不会对func2()进行优化。

二、常用的优化程序的方法

由于编译器优化程序的局限性,在很多时候需要程序员对代码进行显式修改来优化程序。我们定义了一个示例程序,并在此示例程序的基础上逐渐优化。

首先定义了一个集合vec_rec,包含一个数组指针以及数组长度,其指针为vec_ptr:

typedef struct
{
long int len;
data_t *data;
}vec_rec, *vec_ptr;


为了使用不同数据类型,我们将数据类型定义为data_t,在实际运用中可以修改。

typedef int data_t;


定义两组变量分别实现加运算和乘运算:

#define IDENT o
#define OP +

#define IDENT 1
#define OP *


然后定义两个函数分别用来获得数组元素以及返回数组长度。

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(vec_ptr v)//获得数组长度
{
return v->len;
}
下面定义合并运算,将数组中元素相加或相乘:

void combine1(vec_ptr v, data_t *dest)
{
long int i;
*dest = IDENT;
for(i = 0; i < vec_length(v); i++)
{
data_t val;
get_vec_element(v, i, &val);
*dest  = *dest OP val;
}
}
1、消除循环的低效率

上述函数combine1调用函数vec_length作为for循环的测试条件。因此,每次循环迭代都必须对测试条件求值。同时我们注意到,向量的长度并不会随循环的进行而改变。因此,只需要计算一次向量的长度,然后我们在测试条件中都使用这个值即可。

这个优化是一类常见的优化的一个例子,称为代码移动。这类优化包括识别要执行多次但计算结果不会改变的计算。因此可以将计算移动到代码前面不会被多次求值的部分。

2、减少过程(函数)调用

过程调用会带来相当大的开销,而且妨碍大多数形式的程序优化。如果程序的每次循环中都有函数调用,则必然会导致程序运行的缓慢。一种改进方法是构造一个返回数组的函数,在循环外调用这个函数,得到一个数组,在循环体内只要使用这个数组中的元素即可。

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

由于计算机读写存储器的速度非常慢,所以程序中应该尽量避免不必要的存储器引用。一种方法是引入临时变量,用来保存中间数值,如循环累加的数值等等。

4、使用循环展开

循环展开是一种程序变换,通过增加每次迭代计算的元素数量,减少循环的迭代次数。循环展开能从两个方面改善程序的性能。首先,它减少了不直接有助于程序结果的操作的数量,例如循环索引计算和条件分支。其次,它提供了一些方法,可以进一步变化代码,减少整个计算中关键路径上的操作数量。

5、提高并行性

提高并行性采用的措施是分割关键路径。程序运行过程中都存在一条关键路径,我们可以将这条关键路径切割成不同长度的子路径,在不同CPU中运行,从而提高并行性。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: