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的方式来处理程序异常。