公众号的内容都是作者自己思考的内容和结论。作者只是从自己的视野中观察世界,犹如井底之蛙从井口望向世界,看到的只是自己视野里的东西。
此篇主要介绍热点代码、I/O、以及并行部分的优化,我们会从原理出发来,再根据原理讲优化,这样即学习了原理,又知道了优化的来龙去脉。
热点代码
这里作者带我们聊一聊关于代码细节的优化,虽然语句的细节优化并不能带来非常明显的提升,但是也是非常有必要的优化步骤,尤其在那些追求极致高性能或精小的组件中,代码细节的优化决定了组件与组件之间的差异。
语句细节的优化,其实质是对CPU指令的优化,可以认为是从执行指令流中移除指令的过程。下面先来阐述一下细节优化的原理。
语句的细节优化,其实质是执行指令数量的优化,指令跳转次数的优化,向栈中保存临时寄存器次数的优化,以及内存分配次数的优化。
执行指令数量减少了可以减少CPU在执行程序时的耗时我们很好理解,指令跳转则是因为指令也是被放在内存中的数据,因此它也会被高速缓存cache,长距离跳转会让高速缓存失效,静态函数调用和非成员函数调用通常都是长距离指令跳转的典型案例。
函数调用开销不可忽视,即使一个空函数,在调用时也会有性能开销(编译器可能会帮我们优化掉空函数),有时为了极致的优化,我们应该最大限度的减少调用函数的频率,特别是频率最高的top3。
因为在函数被调用时会保存当前函数的数据,包括参数、局部变量、当前指令地址、临时寄存器和标记寄存器等,每次调用一个函数会做如下处理:
1.当调用函数时,先保存当前函数的临时变量、参数、临时寄存器、标记寄存器。 2.将这些每个要保存的数据都复制到栈中。3.当前执行的地址复制到栈中。4.将指令指针寄存器IP指向要执行的函数体的第一句5.执行函数体中的指令6.将函数调用结果保存到寄存器7.从栈中推出要返回的地址,并复制给指令寄存器IP8.推出栈中的临时寄存器、参数、局部变量、标记寄存器都重新还原回去9.继续执行剩下的指令直到遇到下一个函数。
如果遇到成员函数是虚函数的,还得先从虚表中偏移并取出函数地址再调用,这里又多了2次计算,即先取出虚表地址、再根据虚表地址偏移获得真正的函数地址、最后再才能跳转过去。如果是多重继承、或者是多重继承的继承类中的虚函数成员,则需要再加一次地址偏移计算。
inline内联是减少函数调用的最佳方式,内联函数并不像一般函数那样会保存数据并且跳转指令,因为编译器会就地展开内联函数中的指令,因此没有推栈入栈保存数据到栈和跳转指令到函数再返回的步骤,取而代之的是就地直接执行指令。
这样看来减少函数调用(或让函数内联)的同时也减少了入栈、出栈、复制数据的指令数量,也减少了指令跳转的丢失高速缓存的概率。
不必要的内存分配也是在代码细节中常犯的错误,尤其指向堆内存分配,当函数中需要某个容器或者类实例时,常会临时向堆内存申请一次以用来计算。
我们来看看以上这说的4个细节的具体例子:
for(int i = 0 ; i<strlen(str) ; i++) ...//改为for(int i = 0, n = strlen(str) ; i ...
这里不一定有优化,因为编译器可能会识别这类循环并将实时计算移出去,不过不能保证编译器一定会这么干,所以我们最好做人为的优化,保证不重复计算。
void function(){
list = new list(); for(int i = 0 ; i {
list.Add(xx); } return;}//改为public static list = new list(); //改为全局变量void function(){
list.Clear(); for(int i = 0 ; i {
list.Add(xx); } return;}
函数中临时的堆内存分配,改为全局的共用内存,只要分配一次,每次使用前先清理就能节省开销。
int k = 0;for(int i = 0 ; i<100 ; i++){
int j = sin(100) + cos(50); k = j*i;}//改为int k = 0;int j = sin(100) + cos(5);for(int i = 0 ; i<100 ; i++){
k = j*i;}
移除循环中不变的计算,减少不必要的指令,可能会被编译器优化掉。
for(int i = 0 ; i<10 ; i++){
int b = Add(3,5); ...}//改为int b = 0for(int i = 0 ; i<10 ; i++){
b = 3 + 5; ...}//或者将Add函数内联inline int Add(a,b){
...}
用内联或者手动内联的方式,减少循环中的函数调用开销。
string str = "a";str = "
"