M+=j+i
}
● 表达式求值顺序
I = f(x) + g(x);
是先执行f(x), 还是g(x),不同的编译器有不同的实现,语言本身并不保证表达式内各组件的计算顺序,如确实要先执行f(x),后执行g(x),可如下
Temp1=f(x);
Tmp2=g(x);
J = temp1 + temp2;
语言对求值顺序不作要求是为了编译器在优化时有更大的自由度
● 代码优化不利之处:编译时间长,优化后调试器可能不能工作,优化程序最有可能才生 bug.
四、一些有点意思的Item
● 在C++中,const int i=128 如何在内存中表示(是否是一个只读变量的意思)?与#define 的区别何在?
● 布尔变量的实现
有的用1表示true.0表示false,有的用0表示false,非0表示true.两种方式各有优劣,前者false的非操作为FF,而不是true(1);另外没有CPU提供指令检查=1,检查0和非0却很容易.后者按位逻辑操作有问题,如ture(a5)&(5a)等于0而非期望中的true,true(a5)的非是5a,还是ture而非期望的false.C/C++中未定义true,false这种变量.(p175)
● 全局 static int arr[3] = { 1,2,3},gcc为此变量生成的代码为
LC0:.long 1
.long 2
.long 3
显然编译时就确定并填充了值
而在以下函数中
int f() {
Int arr[3]={1,2,3}
…
}
arr为局部变量,每次call f()时,arr的地址都不一样,编译器通常要造一份常量数据,在call f()时,再将常量memcpy 至arr中,这样会占用额外的空间和时间,到底{1,2,3}放在何处?
如果f()没有修改arr数据,可能以下函数效率更高(只初始化一次)
Int f() {
Static int arr[3]= {1,2,3}
…
}
局部变量初始化的例子如下:
int main( int argc, char **argv )
{
static initStruct staticStruct = {1,2,"Hello", {3,4,5,6}};
initStruct autoStruct = {7,8,"World",{9,10,11,12}};
thwartOpt( &staticStruct );
thwartOpt( &autoStruct );
return 0;
}
局部变量autoSturct的初始化
编译器会自动加入以下指令
mov DWORD PTR _autoStruct$[ebp], 7
mov DWORD PTR _autoStruct$[ebp+4], 8
mov DWORD PTR _autoStruct$[ebp+8], OFFSET FLAT:$SG52957
mov DWORD PTR _autoStruct$[ebp+12], 9
mov DWORD PTR _autoStruct$[ebp+16], 10 ; 0000000aH
mov DWORD PTR _autoStruct$[ebp+20], 11 ; 0000000bH
mov DWORD PTR _autoStruct$[ebp+24], 12 ; 0000000cH
● 应当把被执行可能性大的代码放在前面,如下
If (cond)
…
//运行的可能性为99.%
Else
…// 可能性很小
汇编如下写
Jz aaa
…//可能性为99%
End:
aaa:
…//可能性1%
Jmp end.
因为现代CPU采用流水线操作,即一次读入多个指令,因此顺序执行的代码最快
● 局部变量的访问
F()
{
Int arr[256];
Int k;
…
}
与f()
{
Int k;
Int arr[256];
…
}
后面的函数对于k的访问更快.因为后者的偏移量在128内,只需一个字节即可编码基于EBP的偏移量
● 局部变量与结构都有对齐的问题,
Fun()
{
Char a;
// insert 一个char (pad),因为b须要在2字节的倍数边界上存取
Short b;
Char c;
// insert 三个char (pad)
Int d;
}
Fun()
{
Int d1;
Int d2;
Short w1;
Short w2;
Char b1;
Char b2;
//这种变量布局更节省空间,编译器不会insert char
}
对于数组,编译器常会在数组之前填充字节,以让数组从地址为2或4或8字节的整数部位置开始,目的是达到通过移位操作计算数组元素的偏移量
Struct{
Long a;
Char b
} T
T arr[2] = { {2,3},{4,5} }
.data
.align 2
_T:
.long 2
.byte 3
.space 3
//第一个数组元素后填充3个byte
.long 4
.byte 5
.space 3
//第二个数组元素后填充3个byte
记录中的域可能被insert字节外,记录末尾也可能被insert 字节以保证记录数组中的每个记录都起始于2/4/8倍数地址
参数也可能被对齐,如
Call(char b)
{…
}
可能被push 的是一个4byte
使用packed能关掉域对齐
● 如何操作字符串
尽量使用标准字符串函数,因为它们经过了高度优化;
如多次使用字符串长度,注意用变量先保存起来;
for (i=0;i // 不好
len = strlen(str);for (i=0;i // 先保存字符串长度,好
避免在字符串变量之间copy数据,尽量使用一份源数据;
指针与整数相加,相减,指针与指针相减(类型相同的话),两指针进行比较(类型相同 )都是有意义的,好象指针与指针相加没有意义?
● Malloc
向操作系统直接请求分配内存可能较慢,因为CPU要在核态与用户态之间切换,而切换不快。所在大多数运行库都有自己的malloc,free,即首次分配内存时,会从OS申请一大块内存,malloc,free就管理这一大块内存,如果应用申请的内存太大,malloc 就直接向OS申请
Ptr=Malloc(sizeof(char)),而实际可能分到8个byte,因为malloc有个分配粒度,多分配的7个字节被浪费了,这7个字节被称为内部碎片,它不能被用来再分配,而常说的碎片是指外部碎片,外部碎片还能被用来分配/
● 含虚函数的C++的对象中仅多一个指针的开销,这个指针指向一个虚函数表(VMT),不同的类有不同的VMT,通过VMT找到要调用的成员函数
● 短路求值
C语言支持短路求值
If (ptr!=NULL && *ptr!=’/0’)
Do something;
Else
Printf(“err”);
If ptr为空,则表达式立即为0,不用计算*ptr!=’/0
’
If (ptr==NULL || ptr->errcode )
Printf(“err”);
If (A && B && C)
只有在A为true时才计算B,B为true时才计算C
即如果A为0则立即表达式立即返回为0
If (A||B)
如果A为1则表达式立即为1
全面布尔求值则是要求总是计算出表达式中每个组件的值,PASCAL好象是全面求值的.
● Switch 与 if else的区别
Switch可用跳转表来实现(当然语言并未如此规定),如:
Jmptable:dwoard[4]:= { &lable0,&lable1,&lable2,&lable3 }
Mov(I,eax);
Jmp (jmpTable[eax*4]);
Label0:
…
Jmp switchdone;
Lable1:
…
Jmp switchdone
…
Switchdone:
但是如果i的值不连续,就有点麻烦了
Switch(i) {
Case 1:
Case 8:
Case 100:
Case 1000:
}
那就要定义一张大表
Jmptable:dwoard[1000]:= { &switchdone,&lable1, &switchdone,… }
显然很浪费空间,所以在用switch之时,如果值比较分散,就要谨慎了。
有的聪明的编译器在实现switch时,会根据值的情况将if else 与跳转表结合起来,如:
If (I = 1000) jmp try1000
Jmp(tmptable[eax*4]);
//先分走=1000的分支,再用跳转表
或者
Switch(i) {
Case 0:
Case 1:
Default:
If (i==1000)
Else
}
当case多,跳转表太大时,有的编译器会用二分查找来实现switch(if else 是线性查找)
如果有很多case,且每个case被执行的概率相等,那么跳转表就较高效
如果有一两个case的机率很高,那么if else 通过先检查机率高的case,反而更高效。
总之选择switch还是if else,要看case 的多少,case值的分散程序,各个case处理语句被执行的概率
● 叶子函数
不再调用其它函数的函数称为叶子函数,因为叶子函数不必保留寄存器的值,因此编译器能对这种函数生成效率较高的代码
…
● 参数压栈
Push 3个int参数 (12byte)
Call _hasboth
…
_hasboth:
Push ebp;
Mov esp,ebp;
//这两条instructment为标准入口序列,有了esp,为何还要用到ebp?
…
Mov ebp,esp;
//释放局部变量空间,
Pop ebp;
//恢复ebp
Ret (12);
//标准出口序列
通过push,pop来传递参数因为通过内存access,因此开销较大,直接用register传递就更快,如调用者 mov ax,par,子函数直接用使用ax,当然这样做的前提条件是调用者与被调用者之间有一种约定.