读书笔记: optimizing program performance

文章仅仅使用了O1的优化级别,即使这样作者也可以写出相当于O3的速度的代码甚至比这个还要高, 编译器本身的优化已经很可观了,但是还是不如写得谨慎的代码,因为编译器每做一个优化都要很小心,担心会不会有负面效果,它不能完全的优化到最优的代码组合的程度。

1. optimize blocker 有一些会阻碍编译器自动的优化代码,

    a) memory aliasing,就是当两个指针指向同一内存的时候,

void add1(int* a, int* b){
    *a += *b;   // 2次读  1次写
    *a += *b;
}  // 共进行6次对内存的访问

void add2(int*a, int*b){
    *a += 2 * *b; //  2次读  1次写, 2是immediate varaible, 不需要对内存访问
}

显然如果是这样的情况,编译器仍对其优化,如果a和b指向同一内存,显然会导致错误的结果,add1番了4倍而add2只有3倍,事实上有很多这样的情况会导致编译器无法优化。

    b) function call

void a(){
    return f()+f()+f()+f(); // 调用4次
}

void b(){
   return 4*f(); // 调用1次
}
但是如果
void f() {  ++state; } 

编译器就没有办法优化这种带有状态变化的函数,当然我们可以通过inline( -finline   )来优化这样的代码。


2. expressing program performance如何量化程序效率

clock cycles per element (CPE). 表达了随着处理数据的规模的增加CPU时钟周期增加的速度。


3. 优化的方式

    a)   尽量将loop内的操作移到外面做。

    b)尽量少传递参数,减少函数调用,因为函数调用由于memory aliasing的原因,它很难作出优化。

    c)尽量去处不必要的指针的内存读写,用immediate 变量代替

    文章讲述了一个很好的例子怎么自己优化到了O3的级别,并比较了其汇编的代码,很有启发。


4. 建立在CPU结构上的优化

之前的优化不依赖任何CPU,如果我们还需要更深层次的优化,就需要依赖不同CPU的架构了。

prime 计算的效率


                           Integer                  Single-precision          Double-precision

Operation    Latency Issue         Latency Issue              Latency Issue

Addition            1       0.33                      3     1                                   3     1

Multiplication       3    1                        4     1                                        5    1

Division         11–21  5–13       10–15 6–11                   10–23 6–19 

latency 是运行的时钟周期个数,issue 表示两次operations之间的等待周期,这个是由于pipeline造成的(因为他们不需要等待整个指令运行好了之后才开始另一个,而是几分之一指令时间就可以了)。

5. 展开循环

循环展开可以有效的提高效率,循环展开不一定要全部展开,部分也行,比如从 ++i 到 i+=2 的变化,

gcc可以帮我们做到循环展开   ‘-funroll-loops’.    


6. 提高并行程度(利用pipeline)

这里的并行是利用的指令执行的latency来并行,比如如果一个乘法要三个latency,如果有前后依赖的关系,比如算法要像流水线一样的执行,那么就不能利用上这剩下的两个latency,但是如果我们能拆成多个相互暂时不依赖的流水线,将会产生并行的效果, 下面一段是我看到过最美的代码。

/* Unroll loop by 2, 2-way parallelism */
 void combine6(vec_ptr v, data_t *dest)
{
    long int i;
    long int length = vec_length(v);
    long int limit = length-1;
    data_t *data = get_vec_start(v);
    data_t acc0 = IDENT;
    data_t acc1 = IDENT;

     /* Combine 2 elements at a time */
    for(i=0;i<limit;i+=2){
         acc0 = acc0 OP data[i];
         acc1 = acc1 OP data[i+1];
    }

 /* Finish any remaining elements */
     for (; i < length; i++) {
         acc0 = acc0 OP data[i];
     }
     *dest = acc0 OP acc1;
 }
Figure 5.21 Unrolling loop by 2 and using two-way parallelism. This approach makes use of the pipelining capability of the functional units.
对于上面相同的操作gcc用另一种优化办法做到相近的结果,

acc = (acc OP data[i]) OP data[i+1]; 
// 先算后面两个
acc = acc OP (data[i] OP data[i+1]);
这个理解起来比较困难,之所以可以提高效率,是因为第二次循环的后面的两个相乘,可以和第一次循环的前面的两个相乘同步进行。


7. 一些限制优化的因素

    a) 由于寄存器的数目的限制,不可能无限的并行,如果过多的像利用pileline并行,反而会导致这个数据被塞到stack上,反而使效率变差,所有并行多少个比较合适,依赖不同的CPU的类型。

    b)分支预测错误。



你可能感兴趣的:(读书笔记: optimizing program performance)