第五章 优化程序性能
目录
- 程序慢问题出现在哪里:
程序要实时地处理视频帧或者网络包
计算任务的计算量非常大
- 编写高效的程序需要:
选择一组适当的算法和数据结构
编写出 编译器能够有效优化 以转换成高效可执行代码的源代码。(优化编译器的能力和局限性,例如指针运算和强制类型转换)
- 程序优化步骤:
第一步 消除不必要的工作(不必要的函数调用、条件测试和内存引用)
第二步 利用处理器提供的 指令级并行 能力,同时执行多条指令。(并行)
5.1 优化编译器的能力和局限性
- 妨碍优化的因素
- 对程序使用 安全的优化 (内存别名使用)
在只执行安全的优化中,编译器必须假设不同的指针可能会指向内存中同一个位置。这种两个指针可能指向同一个内存位置的情况称为 内存别名使用(memory aliasing)。
对于一个使用指针变量 p 和 q 的程序,考虑下列代码:
x = 1000;
y = 3000;
*q = y; /* 3000 */
*p = x; /* 1000 */
t1 = *q; /* 3000 or 1000 */
t1 的值最后取决于指针 p 和 q 是否指向内存中同一个位置。当 p 和 q 指向同一个位置时,反复被修改的是同一个单元的数据,先改成 y (3000)后改成 x (1000),最后指针 p 传给t1的值就是1000。
- 函数调用
考虑下列过程:
long f();
long func1(){
return f() + f() + f() + f();
}
long func2(){
return 4 * f();
}
似乎没有什么问题,那么
若有下列代码:
long counter = 0;
long f() {
return counter++;
}
这段代码修改了全局变量的一部分,导致func1的调用会返回 0+1+2+3 = 6 ,而func2的调用会返回4*0 = 0
5.2 表示程序性能
度量标准:每元素的周期数(Cycle Per Element, CPE)。
千兆赫兹(GHs)十亿周期每秒,例如,一个4GHs的时钟其周期为0.25纳秒,或250皮秒。用时钟周期来表示的是执行了多少条指令。
5.4 消除循环的低效率
代码移动(code motion):识别要执行多次(例如在循环里)但是计算结果不会改变的计算。
5.5 减少过程调用 (略)
5.6 消除不必要的内存引用
比如循环累乘,每次迭代用的是需要返回的那个值,那么每次迭代,累积变量的数值都要从内存读出再写入到内存。这样读写很浪费,每次迭代开始时从dest(累积变量)读出的值就是上次迭代最后写入的值。
对于这种情况引入一个临时变量acc,在循环中去迭代累乘,最后循环完成结果再存到dest里。
/*combine3*/
void combine3(vec_ptr v, data_t *dest)
{
long i;
long length = vec_length(v);
data_t *data = get_vec_start(v);
*dest = IDENT;
for (i = 0; i < length; i++) {
*dest = *dest OP data[i];
}
}
conbine3,dest作为累乘变量。
/*combine4*/
void combine4(vec_ptr v, data_t *dest)
{
long i;
long length = vec_length(v);
data_t *data = get_vec_start(v);
data_t acc = IDENT;
for (i = 0; i < length; i++) {
acc = acc OP data[i];
}
*dest = acc;
}
combine4,引入临时变量acc。
若OP为乘法,IDENT为常量1,设 v = [2, 3, 5]是一个有3个元素组成的向量,考虑下面两个函数调用:
combine3(v, get_vec_start(v) + 2);
combine4(v, get_vec_start(v) + 2);
这里get_vec_start(v)指向数组的首元。
两个函数执行结果如下: