文章仅仅使用了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; }
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
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)分支预测错误。