编写高效程序需要做到以下几点:
1) 选择适当的算法和数据结构;
2) 编写出编译器能够有效优化的代码;(需要理解编译器优化的能力和局限性)
3) 将一个任务拆分为多个任务,执行并行计算;
编译器可能受到妨碍优化的因素的影响,从而不能很好地执行优化;妨碍优化的因素就是程序中那些严重依赖于执行环境的方面,程序员应该避免产生妨碍优化的因素,写出简洁的代码,支撑编译器优化;
编译器优化的第一步(不依赖于目标机器):消除不必要的工作,包括消除函数调用、条件测试、内存引用;
编译器优化的第二部(依赖于目标机器):利用处理器的特性,利用指令级并行的能力,同时执行多条指令。降低一个计算不同部分之间的数据相关,增加并行度,就可以同时执行这些部分;
研究汇编代码, 通过确认关键路径来决定执行一个循环所需要的时间(时间下界);关键路径是在循环的反复执行过程中形成的数据相关链;
5.1 优化编译器的能力和局限性
-Og:表示基本的优化;
编译器必须很小心地只对程序使用安全的优化,编译器必须保证在任何情况下,优化前端程序和优化后的程序具有一样的行为;
1) 内存引用导致潜在的内存别名
一般而言,编译器会进行优化以降低内存引用;
两个指针同时指向一个内存位置的情况称为:内存别名使用;由于编译器必须执行安全的优化,编译器必须假设不同的指针可能指向同一个位置。但是一些函数,其两个参数都是指针,这两个指针都指向同一内存地址时候,其运算结果会有不同,基于这种情况, 编译器不会进行优化;
2) 函数调用
一般而言,编译器会进行优化以减少函数调用;
如果一个函数中改变了全局变量的值,优化函数调用次数后,全局变量改变的次数就会不一样。编译器不能假设这个函数不改变全局变量, 所以编译器不会优化函数调用;
编译器会将简单的函数调用优化为内联函数;
gcc 编译器只尝试在单个文件中定义的函数优化成内联;
一些情况下需要阻止编译器执行内联替换:
1) 使用符号调试器来执行评估代码;
2) 使用代码剖析方式评估程序性能,使用内联替换消除的函数调用时无法被正确剖析的;
gcc并不能进行比较激进的优化功能,因此需要程序员自己编译效率高的代码;
5.2 表示程序性能
度量程序性能的指标:每元素的周期数CPE, CPE帮助我们理解迭代程序的循环性能;
5.3 程序示例
typedef long data_t;
typedef struct{
long len;
data_t *data;
}vec_rec, *vec_ptr;
vec_ptr new_vec(lonng len)
{
vec_ptr result = (vec_ptr)malloc(sizeof(vec+rec));
data_t *data = NULL;
if(!result)
return NULL;
result->len = len;
if(len > 0){
data = (data_t *)calloc(len, sizeof(data_t));
if(!data){
free((void *)result);
return NULL;
}
}
result->data = data;
return result;
}
int get_vec_element(vec_ptr v, long index, data_t u&dest)
{
if(index < 0 || index >= v->len)
return 0;
*dest = v->data[index];
return 1;
}
long vec_length(vec_ptr v)
{
return v->len;
}
combine 函数将数组data中的所有数据经过OP运算累积到一个数
void combine1(vec_ptr v, data_t *dest)
{
long i;
*dest = IDENT;
for(i = 0; i < vec_length(v); i++){
data_t val;
get_vec_element(v, i, &val);
*dest = *dest OP val;
}
}
5.4 消除循环中的低效率
原则:尽量减少在循环中的计算、函数调用等消耗资源的代码;
这种优化方式称为代码移动,识别循环中需要多次执行但是计算结果不会改变的计算,将代码移动到循坏外部;
一般的编译器都会优化,进行代码移动。但是对于在哪里调用函数或是调用多少次变换,编译器都会很小心(就是说编译器可能优化,可能不优化);
//combine 减少了函数调用vec_length(v)
void combine2(vec_ptr v, data_t *dest)
{
long i