C++代码优化

4.1 变量存储

1、数据区

可执行文件包含多个区域,有代码区,数据区等。一般的c++编译器,会把全局变量、static变量、float/double/string常量、switch跳表、初始化变量列表、虚函数表等存放到数据区域。int变量一般会存储在代码区,和指令放到一起。

略解释一下初始化变量列表:int d[]={1,2,3};

2、堆栈区

堆栈区域保存函数调用、上下文、局部变量。因为局部变量存储在堆栈区,所以访问局部变量很可能会命中cpu的cache,其速度很快。

3、堆

申请的内存(如通过new)。

4.2 变量优化

1、使用成员初始化和构造初始化列表

它们都可以避免2次赋值(即初始化后再赋值)。如:

pubilc C(): x(10)

{}

std::string str("java");

避免使用:

std::string str="java";

2、堆栈最快

上面已经说过,因为cache的原因,堆栈变量访问速度很快。

Ø 缩短变量周期

让变量更快速的结束,有2个好处:占用的位置可以给下面的变量使用、编译器甚至可以用寄存器来存储变量。

Ø 延期申请

变量距离上一个使用过的变量近,被cache概率高。

3、参数传递

为了降低函数调用的开销,当有多个参数时,最好把参数组合成一个结构。

4、返回变量

Ø 返回构造形式

避免2次拷贝。如:

return string("java");

要比

return "java";

更快。

Ø 用引用代替返回

避免构造对象。在函数调用的时候,把需要返回的对象都用引用传递进来。如:

void func(Object& retObj);

5、变量紧密定义

关联度很高的变量可以定义在一起。举例来说:

int a[N],b[N];

for(int idx=0;idx<N;idx++)

a[idx] = b[idx];

修改成:

struct {

int a,b;

} d[N];

for(int idx=0;idx<N;idx++)

d[idx].a=d[idx].b;

后者因为a,b紧密定义在一起,其访问对cache更友好。

6、类/结构成员顺序

因为默认对齐的原因,成员变量的顺序对对象的空间占用有一定影响。一般把变量按照字节大小从前往后放。比如:

struct{

double d;

int i;

short s;

bool b;

}

其size是16字节。但:

struct{

bool b;

double d;

short s;

int i;

}

其size是20字节。

例外的是数组成员。一般认为数组成员应该往后放。这是因为访问其他变量的时候,相对偏移(结构的初始位置)比较小,代码更短。如:

mov eax, [ebp+10h] 显然比 mov eax, [ebp+256h] 实际代码要短。

4.3 函数内联

函数内联作为编译器最大最好的优化选项,无论在哪里都值得探讨一番。函数内联的好处是节省了保护现场和返回值的开销。编译器并不是万能的,有些函数很容易进行内联,有些函数则很难进行内联。

对编译器友好的函数,一般代码比较短,函数没有递归逻辑。对编译器不友好的函数,显然就是指:函数指针调用、深度递归、虚函数。函数指针调用,会让编译器不知道真实的函数在哪里,既然都不知道函数在哪里,自然无法内联了。虚函数也是一样的问题,编译器不清楚调用的方法在哪里。

有一种策略可以把虚函数变成可以内联的函数,下面在重点讨论这个策略。假设我们的程序如下:

struct CParent{

    virtual int f(){

        return 0;

}

};

struct CChild1: CParent{

int f(){

return 1;

}

};

struct CChild2: CParent{

int f(){

return 2;

}

};

调用语句如下:

int count=0;

vector<CParent*> ds;

vector<CParent*>::iterator iter=ds.begin();

while( iter !=ds.end())

{

count += (*iter)->f();

iter ++;

}

毫无疑问,程序在编译的时候,不可能知道f函数到底是CChild1::f还是CChild2::f。我们通过加一个内置的type,来明确告诉编译器到底是f是哪个真正的函数:

struct CParent{

    int type;

    virtual int f(){

        return 0;

}

};

struct CChild1: CParent{

    CChild1(){

type=1;

    }

int f(){

return 1;

}

};

struct CChild2: CParent{

    CChild2(){

    type=2;

}

    int f(){

return 2;

}

};

inline int f(CParent* p){

switch(p->type){

case 1:

return ((CChild1*)p)->CChild1::f();

case 2:

return ((CChild2*)p)->CChild2::f();

}

return 0;

}

// 调用代码

int count=0;

vector<CParent*> ds;

vector<CParent*>::iterator iter=ds.begin();

while( iter !=ds.end())

{

count += f(*iter);

iter ++;

}

我们仅用一个switch语句就节约了函数调用的各种开销,很值得。事实证明,这种策略可以极大的提高程序效率。

4.4 switch优化

switch有3种替换模式:

Ø if

用if来替换switch。当switch的判断值数量少时,这种策略比较高效。

Ø Jump table,跳表

用一个数组存储case包含的代码,而直接用case的值来跳转到代码位置。如:

switch(d){

case 0:

xxx;

break;

case 1:

yyy;

break;

...

}

变成:

addr={addr_xxx, addr_yyy};

jump addr[d];

但这个策略只适合判断的值比较连续的情况,这是因为数组下标连续。

Ø Lookup table,查找表

查找表适合简单的逻辑,可以预先计算结果,然后直接根据某种逻辑返回结果。但一般需要编程者自己完成设计。比如:

ret ={"a","b","c"}

return ret[d];

最后大家看一下switch语句的独特用法:

4.5 最大概率路径最短化

这次策略的思想我们多次提到。有几种常见的方式来达到这个目的:

Ø switch/if/?x:y

把最常见的情况放在前面,减少其比较次数。

Ø 布尔表达式

在&&表达式时,把最不容易为true的放在前面。在||表达式时,把最容易为true的放在前面。

Ø 函数内cache

可以用一个变量存储最常见的返回结果。如:

static int comm_input, comm_output;

if(input == comm_input)

return comm_output;

xxx;

考虑到计算最常见的输入需要很多额外操作,故我们可以只存储最上一次的结果。

4.6 异常

C++的异常有不少诟病,比如没有finally,出错时难以释放资源。它还需要大量额外的资源,因为需要保存那些变量没有被析构等信息。我们的建议是不使用异常,尽量通过自定义log以及assert的方式来处理程序异常。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值