C/C++ -- 代码技巧及优化

    本人总结了一些平时编程的小技巧和非算法类的优化,希望各位一起讨论,也分享自己的技巧 

    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段流水线加法,加法的每一个段占用一个时钟周期,保证了最大的资源利用率。


     10、linux 锁 :

        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模型的优化等,敬请期待 !!

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值