本人总结了一些平时编程的小技巧和非算法类的优化,希望各位一起讨论,也分享自己的技巧
1、inline/define适量的代码冗余 :
"代码冗余"是一件很令人讨厌的事情,如果你在两个地方看到了同样的代码,第一反应就应该是"重构"他们,不过类似define这种代码"本地替换"的适当冗余,可以加速代码执行,省去了函数调用的事件,让CPU顺序执行。所以适当的define和inline带来的代码冗余还是不错的。
2、适当的goto :
很多书上基本都写过"禁止使用goto"这样的字眼,不过在某些特定情况下,适当使用goto能漂亮的解决问题。在kernel开发中,goto被用于“跳出多重循环”和“错误处理跳转”,使得逻辑更加清晰,只要满足"goto不跨函数"、"goto不向上跳转"的情况下goto还是有用武之地的。
int foo(int value) { if(value<=0) goto err; value *= 100; ... ... if (value>=10000) goto err; // OK return 0; err: LOG_WRITE("Error"); return -1; }
3、do{}while(0)宏 :
往往有的时候写一个"宏函数"能带来匪夷所思的效果:
#define fun(condition) if(condition) dosomething();
现在在程序中这样使用这个宏:
if(temp)
macro(i);
else
doanotherthing();
代码编译成功带式结果出人意料,else的触发非常奇怪,因为else被两个if"搞晕了"。这种现象很常见,甚至可能导致代码根本编译不通过。解决的方法就是用kernel的style用do{}while(0)包住这个宏,就不会有问题了:
#define fun(blah) do { \ printf("it is %s/n", blah); \ do_something_useful(blah); \ } while (0);
4、使用三元运算符替代if else :
有的时候if else只是一句简单判断而已,比如 :
if (key==1) doSomething(); else doSomethingElse();这样CPU就不是顺序执行力,而且还会因为逻辑带来一些性能损耗,看着也不是很舒服,可以用 ?: 来替换:
key==1 ? doSomething() : doSomethingElse();
5、if else增加预判机制 likely宏 :
想象如下一个场景,一个for循环1~1000,找出100的整倍数的数字,很简单for+if(num%100) else就搞定了,但是这里有优化的地方,因为if else中else大部分时候都会被执行,if里的代码被执行的概率之后1%,这样就可以做if条件的预判,linux kernel中通过使用gcc的内置功能,使得认为可以预判if else的分布情况:
#define likely(x) __builtin_expect((x),1) #define unlikely(x) __builtin_expect((x),0)
likely表示x为真的可能性大,unlikely相反。这样,编译器就可以把if后面的语句的地址直接放到cpu的下一条指令上并缓存(因为编程者告诉gcc为真的可能性大),这样,某种程度上就把分支结构变成了顺序结构(cpu的cache会命中),只有在1%的情况下才会重新去内存都指令。
if (likely(x%100)) printf("no"); else printf("Yes");
6、cache命中 :
和上面类似,这小节主要是希望大家在编程序时候,能尽量从CPU的角度触发,因为CPU最喜欢顺序操作,以为下一条指令或者下一条数据总是会被cpu的一级cache和二级cache读进去,如果命中率就不会再有内存的存取。
7、(栈)数组cache :
能尽量用栈空间就用栈空间,能尽量用数组就用数组。栈是自动由c的运行时来托管的,不必担心碎片和泄露的问题,速度也是比堆快很多的(具体数量级没测试)。并且在栈释放时是作为一块大内存(通常栈在linux中默认是8M,windows是1M),整体还给操作系统的不像malloc等一片一片还给系统,这里牵扯到内存池的某些概念,后续的文章会详细介绍。还有就是数组,如果能预知长度最好用数组,同样的道理因为内存是连续的,方便cpu去cache并且可以通过索引直接定位,比如array[x]。
8、结构体对齐 :
为了cpu对内存读数据时的高效性,gcc默认是开启字节对齐的,具体数值(2或者4或者8)取决于操作系统的位数,也可以通过__align_of__宏进行设置。有时候,利用字节对齐,我们可以做一些优化。比如,linux kernel的红黑树的节点声明如下:
struct rb_node { unsigned long rb_parent_color; #define RB_RED 0 #define RB_BLACK 1 struct rb_node *rb_right; struct rb_node *rb_left; } __attribute__((aligned(sizeof(long))));这里有一个小技巧,rb_parent_color其实没那么简单,正如名字所暗示,这个成员其实包含指向parent的指针和此结点的颜色!整个结构体是按sizeof(long)大小的对齐,所以任何rb_node结构体的地址的低两位肯定都是零(内存分配的时候得到的首地址肯定是4的整倍数,即时申请3个字节,因为系统考虑了效率和防止碎片化),就可以用它们表示颜色,反正颜色就两种,一位就已经够了。
#define rb_parent(r) ((struct rb_node *)((r)->rb_parent_color & ~3)) // 去除后两位,得到的就是指针 // 得到颜色只要看最后一位即可 #define rb_color(r) ((r)->rb_parent_color & 1)有时候,我们malloc的对象不一定用完就free,可能要做一个refcount(引用计数,可以自己baidu一下),等0==refcount了,在真正free释放(对象的重用)。如果我们的系统是x64的,我们的指针就会是8个byte,寻址的范围都上TB,也就是说头几位根本用不到,就可以用这位做计数器(用法同上,按位&一下就出来)。
9、switch优化 :
一、把可能的值尽可能往前放。
二、可以switch套着switch这样就把一串switch进行分层了。
三、如果是字符串,可以用第一个字母做索引,再进行分层,最外层最多24+10个(字母和数字)。
四、巧用case击穿,某些case如果完全等价的话,可以用击穿的方式用同一逻辑处理。
10、并行代码
double a[100], sum1, sum2, sum3, sum4, sum; int i; sum1 = sum2 = sum3 = sum4 = 0.0; for (i = 0; i < 100; i += 4) { sum1 += a[i]; sum2 += a[i+1]; sum3 += a[i+2]; sum4 += a[i+3]; } sum = (sum4+sum3)+(sum1+sum2);
使用4路分解是因为这样使用了4段流水线加法,加法的每一个段占用一个时钟周期,保证了最大的资源利用率。
phtread_mutex_t是互斥量的锁,多个线程只有一个可以访问临界资源。在互联网开发里,一致性没那么高的话,可以多用读写锁来优化,并发性能更好。
在某些业务场景下,比如cpu消耗型的,可以考虑用spinlock自旋锁来让cpu忙等,这样省去了cpu之间切换的代价(实验证明某些情况下效果还是比较明显),比如memcache进程,使用率SpinLock()的方法,官方文档的数据QPS增加了将近20%。linux用户态下可以自己实现一个很简单spinlock(),代码就是while(1) tryLock(lock);
11、c++初始化列表
多用初始化列表给私有元赋初值,在构造函数里赋值不是赋初值,所以尽量在初始化列表里做。
12、智能指针
std::auto_ptr<E>/ std::shared_ptr<E> / boost::shared_ptr<E>都是智能指针,但是,强烈建议使用boost,如果只是用standard c++,那么就用std::shared_ptr<E>,auto_ptr是不能赋值的指针(比如autop1=autpp2),会发生隐晦的权利转移,并且不能用于标准stl容器。std::share_ptr<E>可以赋值和赋值,并且可以放入stl容器。
后续还有数据结构的优化、内存池的优化、网络I/O模型的优化等,敬请期待 !!