ykn5163713
从oracle到thomsonreuters,原本只是想看看人家到底是怎么用oracle的,结果......
关注
1.对寄存器的访问的优化
寄存器访问优化的核心是尽量避免打断流水线.
a.指令之间数据依赖的时候(如存在对同一个寄存器的读写依赖)
b.指令之间存在控制依赖的时候(如条件转移)
注意的是,并不是只有两条相邻的指令才会存在依赖.只要是同时出现在流水线中任意两条指令都可能出现依赖.
以F(fetch) D(decode) E(execute) M(access memory) W(write back) 模型为例.(任意两个流水阶段之前都有一个cache,一般称为流水线寄存器)
D阶段任务中有一部分是要读出指令中引用的应寄存器中的内容(例如要取srcA寄存器).而寄存器的内容可能在E,M阶段改变(写).
假设D阶段正常只要1个时钟周期就可以结束,但是srcA寄存器的内容,被前一条指令在E阶段修改了(在流水线中相邻的两个阶段对就代码中相邻的两条指令).通过数据转发(流水线中通过反馈回路将各阶段中对寄存器的修改直接送回给之后的流水线阶段,这些值在延迟到W阶段才会写回到寄存器文件.例如,E阶段修改的值送回给D;M阶段修改的值送回给E,D;W阶段修改的值送回给M,E,D),要等到E阶段执行结束后送回给D.
但是E阶段有时候可能要多个指令周才能结束,比如浮点加法要3个时钟周期.也就是说D必须要3个时钟周期后才能完成.
对于控制依赖,最常见的就是分支预测.
对应的如果在E阶段之后,发现预测的分支是错误的,就要将之后所有在流水线中的指令都"清空"(只要保证之后的指令没有修改"程序员可见状态"的前提下,将对应的流水线中的指令变成NOP指令就可以)
2.对存储器访问的优化
核心是避免出现读写依赖.
方法:
找到一小段程序的"关键路径",画数据流图
关键路径一般是数据流图中
a.对同一寄存器读写.(比如下一步/迭代要读取上一步/迭代修改过的寄存器)
b.对同一存储器位置的读写.(比如下一步/迭代要读取上一步/迭代修改过的存储器)
特别的是对循环来说,确定两次迭代之间有没有这种数据依赖,可以计算出CPE(circle per execution)下界
常见优化
- 确定对寄存器,存储器的别名
别名一般是两个指针,指向同一段内存, 或者两个指针保存在同一个寄存器中.
e.g. void swap(int *xp, int *yp){
xp = *xp + *yp;
*yp = *xp - *yp;
*xp = *xp - *yp;
}
这个函数,打眼一看你是操作两个指针.
但是调用的时候,可以给两个参数传入相同的指针.
基于这种假设,编译器认为这个指针变量可能对同一个寄存器的操作.
void twiddle(int *xp, int *yp) {
*xp += *yp;
*xp += *yp;
}
打眼一看,上面这个函数可能等于
void twiddle1(int *xp, int *yp) {
*xp = 2 (yp);
}
但是如果xp,yp是相同的话,这个优化就是错的.
2.确定函数的调用是不是有副作用
因为编译器很能检测出一个函数调用是不是有副作用,所以它会假设所有的函数调用都有.
int f();
int func() {return f() + f() + f();}
如果没有副作用的话,可以优化成 int func() {return 3f();}
但是如果f()有副作用的话,(例如每次调用都会修改某个全局变量),那么这个优化就是错的.
3.减少函数调用(特别是在循环中)
4.消除不必要的存储器引用
(通过增加临时变量)
5.循环展开
6.提高并行性(使用多个临时变量,只在必要的时候将多个临时变量合并)
7.重新结合变换(基于结合律,改变计算顺序,使得后续的计算不依赖前面计算的结果)
e.g.
acc = (acc OP data[i]) OP data[i+1];
每次迭代都要求前一次迭代必须结束才能开始,因为第一个操作就要求读取acc的值.
acc = acc OP (data[i] OP data[i+1]);
修改后,下一次迭代的开始可以先于当前迭代的结束,因为下一次迭代可以先计算data[i] OP data[i+1]这个操作.
最后,再次强调Amdahl定律
S=1/((1-a) + a/k);
S为加速比,
a为可加速部分的百分比
K为对a部分加速的倍数
这个定律告诉我们,在调优的过程中要关注"热点"