读编程卓越之道(二)

 
 
 
 
一、
程序中有常量和变量两种,有的常量存在于内存中,有的常量直接编入指令中,后者更有效率
A:
oneThousand = 1000
x=x+oneThousand          // mov(oneThousand,eax);add(eax,x);
B:
y=y+1000                      // add(1000,y) 采用立即数寻址将1000直接整合在指令中
显然B的效率较A高效
 
二、CISC和RISC的区别:
     RISC采用调入存储体制,即只允许通过调入和存储指令访问内存,其它指令一概只能操作寄存器里的数据,而CISC的指令则可直接操作内存数据
     RISC仅提供两种寻址:寄存器加位移量( lbz r3,4(r5) // [r5+4]->r3 );寄存器加寄存器 (lbzx r3,r5,r6 // [r5+r6]->r3 )
     X86(RISC的代表)实现了硬件栈,PowerPC(RISC的代表)的栈是软件栈
     CISC哲学就是要让每条指令做尽量多的事情,而RISC则是让每条指令只完成一个操作。比如CISC提供了push,pop,push的执行很复杂,而RISC只有bl,blr,要实现push的功能还需要其它指令。但有时候当函数很简单时,其实并不需要push,pop,只要有bl,blr就行了,这时RISC就比CISC快。打个比方:RISC提供了做房子的积木,而CISC却可能是直接提供了一个房子。
 
三、编译器常见优化措施
     常量折叠
编译时就计算出常量表达式的值,而不是运行时才计算
bigArr[16*10] // =bigArr[160]
char str1=”aaaa”;
char str2=”aaaa”;
编译器让str1与str2 point to the same address.
 
     常量传播
在编译时若能确定变量值,就把变量替换为常量值
i = 5;
x = f(i);
优化后变成
i=5
x=f(5)
     死码消除
源代码从未被用过,或条件从不为true时,就删掉此目标码
Int func()
{
Return(3);
}
 
Main()
{
Int i=func();           //程序未用到I,因此此句优化时将会被删除
Return(0);
}
 
     公共子表达式消除
Expr1 = (i+j) * (k+m+n);
Expr2= (i+j);
Expr3=(k+m+n);
好的编译器会在计算expr1后将i+j的值保留,计算expr2时不用重新计算
 
     强度削弱
用shift,and代替乘法,除法,能用unsigned 时尽量用,比有符号型的更容易优化循环不变量
 
     将循环不变量移出循环体
For (i=0;i<k;++i) {
J=k+2;                    //循环不变体
M+=j+i
}
被优化为
J=k+2;
For (i=0;i<k;++i) {
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<strlen(str);i++)                            // 不好
len = strlen(str);for (i=0;i<len;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,当然这样做的前提条件是调用者与被调用者之间有一种约定.
 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值