网站建设的自我总结,办公空间设计要素,当今做网站的流行,微信商城怎么开店C代码优化方案 华中科技大学计算机学院 姓名#xff1a; 王全明
QQ#xff1a; 375288012
Email#xff1a; quanming1119163.com 目录
目录
C代码优化方案
1、选择合适的算法和数据结构
2、使用尽量小的数据类型
3、减少运算的强度
#xff08;1…C代码优化方案 华中科技大学计算机学院 姓名 王全明
QQ 375288012
Email quanming1119163.com 目录
目录
C代码优化方案
1、选择合适的算法和数据结构
2、使用尽量小的数据类型
3、减少运算的强度
1、查表(游戏程序员必修课)
2、求余运算
3、平方运算
4、用移位实现乘除法运算
5、避免不必要的整数除法
6、使用增量和减量操作符
7、使用复合赋值表达式
8、提取公共的子表达式
4、结构体成员的布局
1按数据类型的长度排序
2把结构体填充成最长类型长度的整倍数
3按数据类型的长度排序本地变量
4把频繁使用的指针型参数拷贝到本地变量
5、循环优化
1、充分分解小的循环
2、提取公共部分
3、延时函数
4、while循环和do…while循环
6、循环展开
6、循环嵌套
7、Switch语句中根据发生频率来进行case排序
8、将大的switch语句转为嵌套switch语句
9、循环转置
10、公用代码块
11提升循环的性能
12、选择好的无限循环
6、提高CPU的并行性
1使用并行代码
2避免没有必要的读写依赖
7、循环不变计算
8、函数
1Inline函数
2不定义不使用的返回值
3减少函数调用参数
4所有函数都应该有原型定义
5尽可能使用常量(const)
6把本地函数声明为静态的(static)
9、采用递归
10、变量
1register变量
2、同时声明多个变量优于单独声明变量
3、短变量名优于长变量名应尽量使变量名短一点
4、在循环开始前声明变量
11、使用嵌套的if结构 C代码优化方案 1、选择合适的算法和数据结构 选择一种合适的数据结构很重要如果在一堆随机存放的数中使用了大量的插入和删除指令那使用链表要快得多。数组与指针语句具有十分密切的关系一般来说指针比较灵活简洁而数组则比较直观容易理解。对于大部分的编译器使用指针比使用数组生成的代码更短执行效率更高。 在许多种情况下可以用指针运算代替数组索引这样做常常能产生又快又短的代码。与数组索引相比指针一般能使代码速度更快占用空间更少。使用多维数组时差异更明显。下面的代码作用是相同的但是效率不一样。 数组索引 指针运算 For(;;){ parray Aarray[t]; for(;;){ a*(p); 。。。。。。。。。 。。。。。。 } } 指针方法的优点是array的地址每次装入地址p后在每次循环中只需对p增量操作。在数组索引方法中每次循环中都必须根据t值求数组下标的复杂运算。 2、使用尽量小的数据类型 能够使用字符型(char)定义的变量就不要使用整型(int)变量来定义能够使用整型变量定义的变量就不要用长整型(long int)能不使用浮点型(float)变量就不要使用浮点型变量。当然在定义变量后不要超过变量的作用范围如果超过变量的范围赋值C编译器并不报错但程序运行结果却错了而且这样的错误很难发现。 在ICCAVR中可以在Options中设定使用printf参数尽量使用基本型参数(%c、%d、%x、%X、%u和%s格式说明符)少用长整型参数(%ld、%lu、%lx和%lX格式说明符)至于浮点型的参数(%f)则尽量不要使用其它C编译器也一样。在其它条件不变的情况下使用%f参数会使生成的代码的数量增加很多执行速度降低。 3、减少运算的强度 1、查表(游戏程序员必修课) 一个聪明的游戏大虾基本上不会在自己的主循环里搞什么运算工作绝对是先计算好了再到循环里查表。看下面的例子 旧代码 long factorial(int i) { if (i 0) return 1; else return i * factorial(i - 1); } 新代码 static long factorial_table[] {1 1 2 6 24 120 720 /* etc */ }; long factorial(int i) { return factorial_table[i]; } 如果表很大不好写就写一个init函数在循环外临时生成表格。 2、求余运算 aa%8; 可以改为 aa7; 说明位操作只需一个指令周期即可完成而大部分的C编译器的“%”运算均是调用子程序来完成代码长、执行速度慢。通常只要求是求2n方的余数均可使用位操作的方法来代替。 3、平方运算 apow(a, 2.0); 可以改为 aa*a; 说明在有内置硬件乘法器的单片机中(如51系列)乘法运算比求平方运算快得多因为浮点数的求平方是通过调用子程序来实现的在自带硬件乘法器的AVR单片机中如ATMega163中乘法运算只需2个时钟周期就可以完成。既使是在没有内置硬件乘法器的AVR单片机中乘法运算的子程序比平方运算的子程序代码短执行速度快。 如果是求3次方如 apow(a3。0); 更改为 aa*a*a 则效率的改善更明显。 4、用移位实现乘除法运算 aa*4; bb/4; 可以改为 aa2; bb2; 通常如果需要乘以或除以2n都可以用移位的方法代替。在ICCAVR中如果乘以2n都可以生成左移的代码而乘以其它的整数或除以任何数均调用乘除法子程序。用移位的方法得到代码比调用乘除法子程序生成的代码效率高。实际上只要是乘以或除以一个整数均可以用移位的方法得到结果如 aa*9 可以改为 a(a3)a 采用运算量更小的表达式替换原来的表达式下面是一个经典例子: 旧代码: x w % 8; y pow(x 2.0); z y * 33; for (i 0;i MAX;i) { h 14 * i; printf(%d h); } 新代码: x w 7; /* 位操作比求余运算快 */ y x * x; /* 乘法比平方运算快 */ z (y 5) y; /* 位移乘法比乘法快 */ for (i h 0; i MAX; i) { h 14; /* 加法比乘法快 */ printf(%d h); } 5、避免不必要的整数除法 整数除法是整数运算中最慢的所以应该尽可能避免。一种可能减少整数除法的地方是连除这里除法可以由乘法代替。这个替换的副作用是有可能在算乘积时会溢出所以只能在一定范围的除法中使用。 不好的代码 int i j k m m i / j / k 推荐的代码 int i j k m m i / (j * k) 6、使用增量和减量操作符 在使用到加一和减一操作时尽量使用增量和减量操作符因为增量符语句比赋值语句更快原因在于对大多数CPU来说对内存字的增、减量操作不必明显地使用取内存和写内存的指令比如下面这条语句 xx1; 模仿大多数微机汇编语言为例产生的代码类似于 move Ax ;把x从内存取出存入累加器A add A1 ;累加器A加1 store x ;把新值存回x 如果使用增量操作符生成的代码如下 incr x ;x加1 显然不用取指令和存指令增、减量操作执行的速度加快同时长度也缩短了。 7、使用复合赋值表达式 复合赋值表达式(如a-1及a1等)都能够生成高质量的程序代码。 8、提取公共的子表达式 在某些情况下C编译器不能从浮点表达式中提出公共的子表达式因为这意味着相当于对表达式重新排序。需要特别指出的是编译器在提取公共子表达式前不能按照代数的等价关系重新安排表达式。这时程序员要手动地提出公共的子表达式在VC.NET里有一项“全局优化”选项可以完成此工作但效果就不得而知了。 不好的代码 float a b c d e f 。。。 e b * c / d f b / d * a 推荐的代码 float a b c d e f 。。。 const float t(b / d) e c * t f a * t 不好的代码 float a b c e f 。。。 e a / c f b / c 推荐的代码 float a b c e f 。。。 const float t(1.0f / c) e a * t f b * t 4、结构体成员的布局 很多编译器有“使结构体字双字或四字对齐”的选项。但是还是需要改善结构体成员的对齐有些编译器可能分配给结构体成员空间的顺序与他们声明的不同。但是有些编译器并不提供这些功能或者效果不好。所以要在付出最少代价的情况下实现最好的结构体和结构体成员对齐建议采取下列方法 1按数据类型的长度排序 把结构体的成员按照它们的类型长度排序声明成员时把长的类型放在短的前面。编译器要求把长型数据类型存放在偶数地址边界。在申明一个复杂的数据类型 (既有多字节数据又有单字节数据)时应该首先存放多字节数据然后再存放单字节数据这样可以避免内存的空洞。编译器自动地把结构的实例对齐在内存的偶数边界。 2把结构体填充成最长类型长度的整倍数 把结构体填充成最长类型长度的整倍数。照这样如果结构体的第一个成员对齐了所有整个结构体自然也就对齐了。下面的例子演示了如何对结构体成员进行重新排序 不好的代码普通顺序 struct { char a[5] long k double x } baz 推荐的代码新的顺序并手动填充了几个字节 struct { double x long k char a[5] char pad[7] } baz 这个规则同样适用于类的成员的布局。 3按数据类型的长度排序本地变量 当编译器分配给本地变量空间时它们的顺序和它们在源代码中声明的顺序一样和上一条规则一样应该把长的变量放在短的变量前面。如果第一个变量对齐了其它变量就会连续的存放而且不用填充字节自然就会对齐。有些编译器在分配变量时不会自动改变变量顺序有些编译器不能产生4字节对齐的栈所以4字节可能不对齐。下面这个例子演示了本地变量声明的重新排序 不好的代码普通顺序 short ga gu gi long foo bar double x y z[3] char a b float baz 推荐的代码改进的顺序 double z[3] double x y long foo bar float baz short ga gu gi 4把频繁使用的指针型参数拷贝到本地变量 避免在函数中频繁使用指针型参数指向的值。因为编译器不知道指针之间是否存在冲突所以指针型参数往往不能被编译器优化。这样数据不能被存放在寄存器中而且明显地占用了内存带宽。注意很多编译器有“假设不冲突”优化开关在VC里必须手动添加编译器命令行/Oa或/Ow这允许编译器假设两个不同的指针总是有不同的内容这样就不用把指针型参数保存到本地变量。否则请在函数一开始把指针指向的数据保存到本地变量。如果需要的话在函数结束前拷贝回去。 不好的代码 // 假设 q ! r void isqrt(unsigned long a unsigned long* q unsigned long* r) { *q a if (a 0) { while (*q (*r a / *q)) { *q (*q *r) 1 } } *r a - *q * *q } 推荐的代码 // 假设 q ! r void isqrt(unsigned long a unsigned long* q unsigned long* r) { unsigned long qq rr qq a if (a 0) { while (qq (rr a / qq)) { qq (qq rr) 1 } } rr a - qq * qq *q qq *r rr } 5、循环优化 1、充分分解小的循环 要充分利用CPU的指令缓存就要充分分解小的循环。特别是当循环体本身很小的时候分解循环可以提高性能。注意:很多编译器并不能自动分解循环。 不好的代码 // 3D转化把矢量 V和 4x4 矩阵 M相乘 for (i 0 i 4 i ) { r[i] 0 for (j 0 j 4 j ) { r[i] M[j][i]*V[j] } } 推荐的代码 r[0] M[0][0]*V[0] M[1][0]*V[1] M[2][0]*V[2] M[3][0]*V[3] r[1] M[0][1]*V[0] M[1][1]*V[1] M[2][1]*V[2] M[3][1]*V[3] r[2] M[0][2]*V[0] M[1][2]*V[1] M[2][2]*V[2] M[3][2]*V[3] r[3] M[0][3]*V[0] M[1][3]*V[1] M[2][3]*V[2] M[3][3]*v[3] 2、提取公共部分 对于一些不需要循环变量参加运算的任务可以把它们放到循环外面这里的任务包括表达式、函数的调用、指针运算、数组访问等应该将没有必要执行多次的操作全部集合在一起放到一个init的初始化程序中进行。 3、延时函数 通常使用的延时函数均采用自加的形式 void delay (void) { unsigned int i; for (i0;i1000;i) ; } 将其改为自减延时函数 void delay (void) { unsigned int i; for (i1000;i0;i--) ; } 两个函数的延时效果相似但几乎所有的C编译对后一种函数生成的代码均比前一种代码少1~3个字节因为几乎所有的MCU均有为0转移的指令采用后一种方式能够生成这类指令。在使用while循环时也一样使用自减指令控制循环会比使用自加指令控制循环生成的代码更少1~3个字母。但是在循环中有通过循环变量“i”读写数组的指令时使用预减循环有可能使数组超界要引起注意。 4、while循环和do…while循环 用while循环时有以下两种循环形式 unsigned int i; i0; while (i1000) { i; //用户程序 } 或 unsigned int i; i1000; do { i--; //用户程序 } while (i0); 在这两种循环中使用do…while循环编译后生成的代码的长度短于while循环。 6、循环展开 这是经典的速度优化但许多编译程序(如gcc -funroll-loops)能自动完成这个事所以现在你自己来优化这个显得效果不明显。 旧代码: for (i 0; i 100; i) { do_stuff(i); } 新代码: for (i 0; i 100; ) { do_stuff(i); i; do_stuff(i); i; do_stuff(i); i; do_stuff(i); i; do_stuff(i); i; do_stuff(i); i; do_stuff(i); i; do_stuff(i); i; do_stuff(i); i; do_stuff(i); i; } 可以看出新代码里比较指令由100次降低为10次循环时间节约了90%。不过注意:对于中间变量或结果被更改的循环编译程序往往拒绝展开(怕担责任呗)这时候就需要你自己来做展开工作了。 还有一点请注意在有内部指令cache的CPU上(如MMX芯片)因为循环展开的代码很大往往cache溢出这时展开的代码会频繁地在CPU的cache和内存之间调来调去又因为cache速度很高所以此时循环展开反而会变慢。还有就是循环展开会影响矢量运算优化。 6、循环嵌套 把相关循环放到一个循环里也会加快速度。 旧代码: for (i 0; i MAX; i) /* initialize 2d array to 0s */ for (j 0; j MAX; j) a[i][j] 0.0; for (i 0; i MAX; i) /* put 1s along the diagonal */ a[i][i] 1.0; 新代码: for (i 0; i MAX; i) /* initialize 2d array to 0s */ { for (j 0; j MAX; j) a[i][j] 0.0; a[i][i] 1.0; /* put 1s along the diagonal */ } 7、Switch语句中根据发生频率来进行case排序 Switch 可能转化成多种不同算法的代码。其中最常见的是跳转表和比较链/树。当switch用比较链的方式转化时编译器会产生if-else-if的嵌套代码并按照顺序进行比较匹配时就跳转到满足条件的语句执行。所以可以对case的值依照发生的可能性进行排序把最有可能的放在第一位这样可以提高性能。此外在case中推荐使用小的连续的整数因为在这种情况下所有的编译器都可以把switch转化成跳转表。 不好的代码 int days_in_month short_months normal_months long_months 。。。。。。 switch (days_in_month) { case 28: case 29: short_months break case 30: normal_months break case 31: long_months break default: cout month has fewer than 28 or more than 31 days endl break } 推荐的代码 int days_in_month short_months normal_months long_months 。。。。。。 switch (days_in_month) { case 31: long_months break case 30: normal_months break case 28: case 29: short_months break default: cout month has fewer than 28 or more than 31 days endl break } 8、将大的switch语句转为嵌套switch语句 当switch语句中的case标号很多时为了减少比较的次数明智的做法是把大switch语句转为嵌套switch语句。把发生频率高的case标号放在一个switch语句中并且是嵌套switch语句的最外层发生相对频率相对低的case标号放在另一个switch语句中。比如下面的程序段把相对发生频率低的情况放在缺省的case标号内。 pMsgReceiveMessage(); switch (pMsg-type) { case FREQUENT_MSG1: handleFrequentMsg(); break; case FREQUENT_MSG2: handleFrequentMsg2(); break; 。。。。。。 case FREQUENT_MSGn: handleFrequentMsgn(); break; default: //嵌套部分用来处理不经常发生的消息 switch (pMsg-type) { case INFREQUENT_MSG1: handleInfrequentMsg1(); break; case INFREQUENT_MSG2: handleInfrequentMsg2(); break; 。。。。。。 case INFREQUENT_MSGm: handleInfrequentMsgm(); break; } } 如果switch中每一种情况下都有很多的工作要做那么把整个switch语句用一个指向函数指针的表来替换会更加有效比如下面的switch语句有三种情况 enum MsgType{Msg1 Msg2 Msg3} switch (ReceiveMessage() { case Msg1; 。。。。。。 case Msg2; 。。。。。 case Msg3; 。。。。。 } 为了提高执行速度用下面这段代码来替换这个上面的switch语句。 /*准备工作*/ int handleMsg1(void); int handleMsg2(void); int handleMsg3(void); /*创建一个函数指针数组*/ int (*MsgFunction [])(){handleMsg1 handleMsg2 handleMsg3}; /*用下面这行更有效的代码来替换switch语句*/ statusMsgFunction[ReceiveMessage()](); 9、循环转置 有些机器对JNZ(为0转移)有特别的指令处理速度非常快如果你的循环对方向不敏感可以由大向小循环。 旧代码: for (i 1; i MAX; i) { 。。。 } 新代码: i MAX1; while (--i) { 。。。 } 不过千万注意如果指针操作使用了i值这种方法可能引起指针越界的严重错误(i MAX1;)。当然你可以通过对i做加减运算来纠正但是这样就起不到加速的作用除非类似于以下情况 旧代码: char a[MAX5]; for (i 1; i MAX; i) { *(ai4)0; } 新代码: i MAX1; while (--i) { *(ai4)0; } 10、公用代码块 一些公用处理模块为了满足各种不同的调用需要往往在内部采用了大量的if-then-else结构这样很不好判断语句如果太复杂会消耗大量的时间的应该尽量减少公用代码块的使用。(任何情况下空间优化和时间优化都是对立的--东楼)。当然如果仅仅是一个(3x)之类的简单判断适当使用一下也还是允许的。记住优化永远是追求一种平衡而不是走极端。 11提升循环的性能 要提升循环的性能减少多余的常量计算非常有用比如不随循环变化的计算。 不好的代码(在for()中包含不变的if()) for( i 。。。 ) { if( CONSTANT0 ) { DoWork0( i )// 假设这里不改变CONSTANT0的值 } else { DoWork1( i )// 假设这里不改变CONSTANT0的值 } } 推荐的代码 if( CONSTANT0 ) { for( i 。。。 ) { DoWork0( i ) } } else { for( i 。。。 ) { DoWork1( i ) } } 如果已经知道if()的值这样可以避免重复计算。虽然不好的代码中的分支可以简单地预测但是由于推荐的代码在进入循环前分支已经确定就可以减少对分支预测的依赖。 12、选择好的无限循环 在编程中我们常常需要用到无限循环常用的两种方法是while (1) 和 for ()。这两种方法效果完全一样但那一种更好呢然我们看看它们编译后的代码 编译前 while (1) 编译后 mov eax1 test eaxeax je foo23h jmp foo18h 编译前 for () 编译后 jmp foo23h 显然for ()指令少不占用寄存器而且没有判断、跳转比while (1)好。 6、提高CPU的并行性 1使用并行代码 尽可能把长的有依赖的代码链分解成几个可以在流水线执行单元中并行执行的没有依赖的代码链。很多高级语言包括C并不对产生的浮点表达式重新排序因为那是一个相当复杂的过程。需要注意的是重排序的代码和原来的代码在代码上一致并不等价于计算结果一致因为浮点操作缺乏精确度。在一些情况下这些优化可能导致意料之外的结果。幸运的是在大部分情况下最后结果可能只有最不重要的位即最低位是错误的。 不好的代码 double a[100] sum int i sum 0.0f for (i0 i100 i) sum a[i] 推荐的代码 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[i1] sum3 a[i2] sum4 a[i3] } sum (sum4sum3)(sum1sum2) 要注意的是使用4 路分解是因为这样使用了4段流水线浮点加法浮点加法的每一个段占用一个时钟周期保证了最大的资源利用率。 2避免没有必要的读写依赖 当数据保存到内存时存在读写依赖即数据必须在正确写入后才能再次读取。虽然AMD Athlon等CPU有加速读写依赖延迟的硬件允许在要保存的数据被写入内存前读取出来但是如果避免了读写依赖并把数据保存在内部寄存器中速度会更快。在一段很长的又互相依赖的代码链中避免读写依赖显得尤其重要。如果读写依赖发生在操作数组时许多编译器不能自动优化代码以避免读写依赖。所以推荐程序员手动去消除读写依赖举例来说引进一个可以保存在寄存器中的临时变量。这样可以有很大的性能提升。下面一段代码是一个例子 不好的代码 float x[VECLEN] y[VECLEN] z[VECLEN] 。。。。。。 for (unsigned int k 1 k VECLEN k ) { x[k] x[k-1] y[k] } for (k 1 k VECLEN k) { x[k] z[k] * (y[k] - x[k-1]) } 推荐的代码 float x[VECLEN] y[VECLEN] z[VECLEN] 。。。。。。 float t(x[0]) for (unsigned int k 1 k VECLEN k ) { t t y[k] x[k] t } t x[0] for (k 1 k VECLEN k ) { t z[k] * (y[k] - t) x[k] t } 7、循环不变计算 对于一些不需要循环变量参加运算的计算任务可以把它们放到循环外面现在许多编译器还是能自己干这件事不过对于中间使用了变量的算式它们就不敢动了所以很多情况下你还得自己干。对于那些在循环中调用的函数凡是没必要执行多次的操作通通提出来放到一个init函数里循环前调用。另外尽量减少喂食次数没必要的话尽量不给它传参需要循环变量的话让它自己建立一个静态循环变量自己累加速度会快一点。 还有就是结构体访问东楼的经验凡是在循环里对一个结构体的两个以上的元素执行了访问就有必要建立中间变量了(结构这样那C的对象呢?想想看)看下面的例子: 旧代码: total a-b-c[4]-aardvark a-b-c[4]-baboon a-b-c[4]-cheetah a-b-c[4]-dog; 新代码: struct animals * temp a-b-c[4]; total temp-aardvark temp-baboon temp-cheetah temp-dog; 一些老的C语言编译器不做聚合优化而符合ANSI规范的新的编译器可以自动完成这个优化看例子: float a b c d f g; 。。。 a b / c * d; f b * g / c; 这种写法当然要得但是没有优化 float a b c d f g; 。。。 a b / c * d; f b / c * g; 如果这么写的话一个符合ANSI规范的新的编译器可以只计算b/c一次然后将结果代入第二个式子节约了一次除法运算。 8、函数优化 1Inline函数 在C中关键字Inline可以被加入到任何函数的声明中。这个关键字请求编译器用函数内部的代码替换所有对于指出的函数的调用。这样做在两个方面快于函数调用第一省去了调用指令需要的执行时间第二省去了传递变元和传递过程需要的时间。但是使用这种方法在优化程序速度的同时程序长度变大了因此需要更多的ROM。使用这种优化在Inline函数频繁调用并且只包含几行代码的时候是最有效的。 2不定义不使用的返回值 函数定义并不知道函数返回值是否被使用假如返回值从来不会被用到应该使用void来明确声明函数不返回任何值。 3减少函数调用参数 使用全局变量比函数传递参数更加有效率。这样做去除了函数调用参数入栈和函数完成后参数出栈所需要的时间。然而决定使用全局变量会影响程序的模块化和重入故要慎重使用。 4所有函数都应该有原型定义 一般来说所有函数都应该有原型定义。原型定义可以传达给编译器更多的可能用于优化的信息。 5尽可能使用常量(const) 尽可能使用常量(const)。C标准规定如果一个const声明的对象的地址不被获取允许编译器不对它分配储存空间。这样可以使代码更有效率而且可以生成更好的代码。 6把本地函数声明为静态的(static) 如果一个函数只在实现它的文件中被使用把它声明为静态的(static)以强制使用内部连接。否则默认的情况下会把函数定义为外部连接。这样可能会影响某些编译器的优化——比如自动内联。 9、采用递归 与LISP之类的语言不同C语言一开始就病态地喜欢用重复代码循环许多C程序员都是除非算法要求坚决不用递归。事实上C编译器们对优化递归调用一点都不反感相反它们还很喜欢干这件事。只有在递归函数需要传递大量参数可能造成瓶颈的时候才应该使用循环代码其他时候还是用递归好些。 10、变量 1register变量 在声明局部变量的时候可以使用register关键字。这就使得编译器把变量放入一个多用途的寄存器中而不是在堆栈中合理使用这种方法可以提高执行速度。函数调用越是频繁越是可能提高代码的速度。 在最内层循环避免使用全局变量和静态变量除非你能确定它在循环周期中不会动态变化大多数编译器优化变量都只有一个办法就是将他们置成寄存器变量而对于动态变量它们干脆放弃对整个表达式的优化。尽量避免把一个变量地址传递给另一个函数虽然这个还很常用。C语言的编译器们总是先假定每一个函数的变量都是内部变量这是由它的机制决定的在这种情况下它们的优化完成得最好。但是一旦一个变量有可能被别的函数改变这帮兄弟就再也不敢把变量放到寄存器里了严重影响速度。看例子 a b(); c(d); 因为d的地址被c函数使用有可能被改变编译器不敢把它长时间的放在寄存器里一旦运行到c(d)编译器就把它放回内存如果在循环里会造成N次频繁的在内存和寄存器之间读写d的动作众所周知CPU在系统总线上的读写速度慢得很。比如你的赛杨300CPU主频300总线速度最多66M为了一个总线读CPU可能要等4-5个周期得。。得。。得。。想起来都打颤。 2、同时声明多个变量优于单独声明变量 3、短变量名优于长变量名应尽量使变量名短一点 4、在循环开始前声明变量 11、使用嵌套的if结构 在if结构中如果要判断的并列条件较多最好将它们拆分成多个if结构然后嵌套在一起这样可以避免无谓的判断。 说明
上面的优化方案由王全明收集整理。很多资料来源与网上出处不祥在此对所有作者一并致谢
该方案主要是考虑到在嵌入式开发中对程序执行速度的要求特别高所以该方案主要是为了优化程序的执行速度。
注意优化是有侧重点的优化是一门平衡的艺术它往往要以牺牲程序的可读性或者增加代码长度为代价。
(任何情况下空间优化和时间优化都是对立的--东楼)。 2.
代码优化概要 我编写程序至今有35年了我做了很多关于程序执行速度方面优化的工(
一个示例)我也看过其它人做的优化。我发现有两个最基本的优化技术总是被人所忽略。 注意这两个技术并不是避免时机不成熟的优化。并不是把冒泡排序变成快速排序算法优化。也不是语言或是编译器的优化。也不是把 i*4写成i2 的优化。 这两个技术是
使用 一个profiler。查看程序执行时的汇编码。 使用这两个技术的人将会成功地写出运行快的代码不会使用这两个技术的人则不行。下面让我为你细细道来。
使用一个 Profiler
我们知道程序运行时的90%的时间是用在了10%的代码上。我发现这并不准确。一次又一次地我发现几乎所有的程序会在1%的代码上花了99%的运行时间。但是是哪个1%一个好的Profiler可以告诉你这个答案。就算我们需要使用100个小时在这1%的代码上进行优化也比使用100个小时在其它99%的代码上优化产生的效益要高得多得多。 问题是什么人们不用profiler不是。我工作过的一个地方使用了一个华丽而奢侈的Profiler但是自从购买这个Profiler后它的包装3年来还是那么的暂新。为什么人们不用我真的不知道。有一次我和我的同事去了一个负载过大的交易所我同事坚持说他知道哪里是瓶颈毕竟他是一个很有经验的专家。最终我把我的Profiler在他的项目上运行了一下我们发现那个瓶颈完全在一个意想不到的地方。 就像是赛车一样。团队是赢在传感器和日志上这些东西提供了所有的一切。你可以调整一下赛车手的裤子以让其在比赛过程中更舒服但是这不会让你赢得比赛也不会让你更有竞争力。如果你不知道你的速度上不去是因为引擎、排气装置、空体动力学、轮胎气压或是赛车手那么你将无法获胜。编程为什么会不同呢只要没有测量你就永远无法进步。 这个世界上有太多可以使用的Profiler了。随便找一个你就可以看到你的函数的调用层次调用的次数以前每条代码的时间分解表甚至可以到汇编级。我看过太多的程序员回避使用Profiler而是把时间花在那些无用的错误的方向上的“优化”而被其竞争对手所羞辱。译者陈皓注使用Profiler时重点需要关注1花时间多的函数以优化其算法2调用次数巨多的函数——如果一个函数每秒被调用300K次你只需要优化出0.001毫秒那也是相当大的优化。这就是作者所谓的1%的代码占用了99%的CPU时间
查看汇编代码
几年前我有一个同事Mary Bailey她在华盛顿大学教矫正代数remedial algebra有一次她在黑板上写下 x 3 5 然后问他的学生“求解x”然后学生们不知道答案。于是她写下__ 3 5 然后再问学生“填空”所有的学生都可以回答了。未知数x就像是一个有魔法的字母让大家都在想“x意味着代数而我没有学过代数所以我就不知道这个怎么做”。 汇编程序就是编程世界的代数。如果某人问我“inline函数是否被编译器展开了”或是问我“如果我写下i*4编译器会把其优化为左移位操作吗”。这个时候我都会建议他们看看编译器的汇编码。这样的回答是不是很粗暴和无用通常在我这样回答了提问者后提问都通常都会说对不起我不知道什么是汇编甚至C的专家都会这么回答。 汇编语言是最简单的编程语言了就算是和C相比也是这样的如
ADD ESI,x
就是C风格的代码
ESI x;
而
CALL foo
则是
foo();
细节因为CPU的种类而不同但这就是其如何工作的。有时候我们甚至都不需要细节只需要看看汇编码的长啥样然后和源代码比一比你就可以知道汇编代码很多很多了。 那么这又如何帮助代码优化举个例子我几年前认识一个程序员认为他应该去发现一个新的更快的算法。他有一个benchmark来证明这个算法并且其写了一篇非常漂亮的文章关于他的这个算法。但是有人看了一下其原来算法以及新算法的汇编发现了他的改进版本的算法允许其编译器把两个除法操作变成了一个。这和算法真的没有什么关系。我们知道除法操作是一个很昂贵的操作并且在其算法中这俩个除法操作还在一个内嵌循环中所以他的改进版的算法当然要快一些。但只需要在原来的算法上做一点点小的改动——使用一个除法操作那么其原来的算法将会和新的一样快。而他的新发现什么也不是。 下一个例子一个D用户张贴了一个 benchmark 来显示 dmd (Digital Mars D 编译器)在整型算法上的很糟糕而ldc (LLVM D 编译器) 就好很多了。对于这样的结果其相当的有意见。我迅速地看了一下汇编发现两个编译器编译出来相当的一致并没有什么明显的东西要对21这么大的不同而负责。但是我们看到有一个对long型整数的除法这个除法调用了运行库。而这个库成为消耗时间的杀手其它所有的加减法都没有速度上的影响。出乎意料地benchmark 和算法代码生成一点关系也没有完全就是long型整数的除法的问题。这暴露了在dmd的运行库中的long型除法的实现很差。修正后就可以提高速度。所以这和编译器没有什么关系但是如果不看汇编你将无法发现这一切。 查看汇编代码经常会给你一些意想不到的东西让你知道为什么程序的性能是那样。一些意想不到的函数调用预料不到的自傲以及不应该存在的东西等等其实所有的一切。但也不需要成为一个汇编代码的黑客才能干的事。
结论
如果你觉得需要程序有更好的执行速度那么最基本的方法就是使用一个profiler和愿意去查看一下其汇编代码以找到程序的瓶颈。只有找到了程序的瓶颈此时才是真正在思考如何去改进的时候比如思考一个更好的算法使用更快的语言优化等等。 常规的做法是制胜法宝是挑选一个最佳的算法而不是进行微优化。虽然这种做法是无可异议的但是有两件事情是学校没有教给你而需要你重点注意的。第一个也是最重要的如果你优化的算法没没有参与到你程序性能中的算法那么你优化他只是在浪费时间和精力并且还转移了你的注意力让你错过了应该要去优化的部分。第二点算法的性能总和处理的数据密切相关的就算是冒泡排序有那么多的笑柄但是如果其处理的数据基本是排好序的只有其中几个数据是未排序的那么冒泡排序也是所有排序算法里性能最好的。所以担心没有使用好的算法而不去测量只会浪费时间无论是你的还是计算机的。 就好像赛车零件的订购速底是不会让你更靠进冠军就算是你正确安装零件也不会没有Profiler你不会知道问题在哪里不去看汇编你可能知道问题所在但你往往不知道为什么。 (全文完) 3.
优化代码 通过优化可执行文件可在较快执行速度和较小代码大小之间实现平衡。 本主题讨论了 Visual C 提供的可帮助您优化代码的一些机制。 语言功能 下面的主题介绍了 C/C 语言中的一些优化功能。 优化杂注和关键字 可在代码中使用以提高性能的关键字和杂注的列表。 按类别列出的编译器选项 专门影响执行速度或代码大小的 /O 编译器选项的列表。 Rvalue Reference Declarator: Rvalue 引用支持移动语义的实现。 如果移动语义用于实现模板库则使用这些模板的应用程序的性能可显著提高。 优化杂注 如果经过优化的某个代码节导致错误或速度减慢则可以使用 optimize 杂注对该代码节关闭优化。 用两个杂注将代码括起来如下所示 #pragma optimize(, off)
// some code here
#pragma optimize(, on)编程惯例 在用优化的方式编译代码时您可能会注意到一些附加的警告消息。 此行为是预期行为因为一些警告仅与优化的代码有关。 如果您注意到这些警告则可以避免许多优化问题。 矛盾的是为了速度而对程序进行优化可能会导致代码运行速度减慢。 这是因为一些为了速度而进行的优化会增加代码大小。 例如内联函数可消除函数调用的开销。但是内联太多代码可能会使程序很大致使虚拟内存页的错误数增加。 因此通过消除函数调用获得的速度可能会丢失在内存交调中。 下面的主题讨论了良好的编程做法。 提高时间关键代码的技巧 更好的编码技术可产生更好的性能。 本主题建议了一些可帮助您确保时间关键代码部分的执行令人满意的编码技术。 优化最佳做法 提供了有关如何以最佳方式优化应用程序的一般准则。 调试优化的代码 由于优化可能会更改编译器创建的代码因此建议您调试应用程序并测量其性能随后优化代码。 下面的主题提供有关如何进行调试的基本信息。 使用 Visual Studio 进行调试 创建发行版本时遇到的常见问题 下面的主题提供有关如何进行调试的更高级信息。 如何调试优化的代码 为何浮点数可能丢失精度 以下各个主题提供有关如何优化生成、加载和执行代码的信息。 提高编译器吞吐量 使用没有 () 的函数名不产生代码 Optimizing Inline Assembly 为 ATL 项目指定编译器优化 加载时应使用哪些优化技术来提高客户端应用程序的性能 有关以下内容的更多信息如何缩短 DLL 方法加载时间的更多信息请参见 MSDN 库网站上的“MSDN 杂志”中“Under the Hood”深入实质专栏下的“Optimizing DLL Load Time Performance”优化 DLL 加载时间性能。 有关以下内容的更多信息如何在应用程序中最大程度减少分页的更多信息请参见 MSDN 库 网站上的“MSDN 杂志”中“Bugslayer”专栏下的“Improving Runtime Performance with the Smooth Working Set Tool”使用 Smooth 工作集工具提高运行时性能和“Improving Runtime Performance with the Smooth Working Set Tool—Part 2”使用 Smooth 工作集工具提高运行时性能第 2 部分。 4.
C代码优化方法总结 优化是一个非常大的主题本文并不是去深入探讨性能分析理论算法的效率况且我也没有这个能力。我只是想把一些可以简单的应用到你的C代码中的优化技术总结在这里这样当你遇到几种不同的编程策略的时候就可以对每种策略的性能进行一个大概的估计。这也是本文的目的之所在。
一. 优化之前 在进行优化之前我们首先应该做的是发现我们代码的瓶颈bottleneck在哪里。然而当你做这件事情的时候切忌从一个debug-version进行推断因为debug-version中包含了许多额外的代码。一个debug-version可执行体要比release-version大出40%。那些额外的代码都是用来支持调试的比如说符号的查找。大多数实现都为debug-version和release-version提供了不同的operator new以及库函数。而且一个release-version的执行体可能已经通过多种途径进行了优化包括不必要的临时对象的消除循环展开把对象移入寄存器内联等等。 另外我们要把调试和优化区分开来它们是在完成不同的任务。 debug-version 是用来追捕bugs以及检查程序是否有逻辑上的问题。release-version则是用来做一些性能上的调整以及进行优化。 下面就让我们来看看有哪些代码优化技术吧
二. 声明的放置 程序中变量和对象的声明放在什么位置将会对性能产生显著影响。同样对postfix和prefix运算符的选择也会影响性能。这一部分我们集中讨论四个问题初始化v.s 赋值在程序确实要使用的地方放置声明构造函数的初始化列表prefix v.s postfix运算符。 1 请使用初始化而不是赋值 在C语言中只允许在一个函数体的开头进行变量的声明然而在C中声明可以出现在程序的任何位置。这样做的目的是希望把对象的声明拖延到确实要使用它的时候再进行。这样做可以有两个好处1. 确保了对象在它被使用前不会被程序的其他部分恶意修改。如果对象在开头就被声明然而却在20行以后才被使用的话就不能做这样的保证。2. 使我们有机会通过用初始化取代赋值来达到性能的提升从前声明只能放在开头然而往往开始的时候我们还没有获得我们想要的值因此初始化所带来的好处就无法被应用。但是现在我们可以在我们获得了想要的值的时候直接进行初始化从而省去了一步。注意或许对于基本类型来说初始化和赋值之间可能不会有什么差异但是对于用户定义的类型来说二者就会带来显著的不同因为赋值会多进行一次函数调用----operator 。因此当我们在赋值和初始化之间进行选择的话初始化应该是我们的首选。 2 把声明放在合适的位置上 在一些场合通过移动声明到合适的位置所带来的性能提升应该引起我们足够的重视。例如 bool is_C_Needed(); void use() { C c1; if (is_C_Needed() false) { return; //c1 was not needed } //use c1 here return; } 上面这段代码中对象c1即使在有可能不使用它的情况下也会被创建这样我们就会为它付出不必要的花费有可能你会说一个对象c1能浪费多少时间但是如果是这种情况呢C c1[1000];我想就不是说浪费就浪费了。但是我们可以通过移动声明c1的位置来改变这种情况 void use() { if (is_C_Needed() false) { return; //c1 was not needed } C c1; //moved from the blocks beginning //use c1 here return; } 怎么样程序的性能是不是已经得到很大的改善了呢因此请仔细分析你的代码把声明放在合适的位置上它所带来的好处是你难以想象的。 3 初始化列表 我们都知道初始化列表一般是用来初始化const或者reference数据成员。但是由于他自身的性质我们可以通过使用初始化列表来实现性能的提升。我们先来看一段程序 class Person { private: C c_1; C c_2; public: Person(const C c1, const C c2 ): c_1(c1), c_2(c2) {} }; 当然构造函数我们也可以这样写 Person::Person(const C c1, const C c2) { c_1 c1; c_2 c2; } 那么究竟二者会带来什么样的性能差异呢要想搞清楚这个问题我们首先要搞清楚二者是如何执行的先来看初始化列表数据成员的声明操作都是在构造函数执行之前就完成了在构造函数中往往完成的只是赋值操作然而初始化列表直接是在数据成员声明的时候就进行了初始化因此它只执行了一次copy constructor。再来看在构造函数中赋值的情况首先在构造函数执行前会通过default constructor创建数据成员然后在构造函数中通过operator 进行赋值。因此它就比初始化列表多进行了一次函数调用。性能差异就出来了。但是请注意如果你的数据成员都是基本类型的话那么为了程序的可读性就不要使用初始化列表了因为编译器对两者产生的汇编代码是相同的。 4 postfix VS prefix 运算符 prefix运算符和—比它的postfix版本效率更高因为当postfix运算符被使用的时候会需要一个临时对象来保存改变以前的值。对于基本类型编译器会消除这一份额外的拷贝但是对于用户定义类型这似乎是不可能的。因此请你尽可能使用prefix运算符。
三. 内联函数 内联函数既能够去除函数调用所带来的效率负担又能够保留一般函数的优点。然而内联函数并不是万能药在一些情况下它甚至能够降低程序的性能。因此在使用的时候应该慎重。 1我们先来看看内联函数给我们带来的好处从一个用户的角度来看内联函数看起来和普通函数一样它可以有参数和返回值也可以有自己的作用域然而它却不会引入一般函数调用所带来的负担。另外它可以比宏更安全更容易调试。 当然有一点应该意识到inline specifier仅仅是对编译器的建议编译器有权利忽略这个建议。那么编译器是如何决定函数内联与否呢一般情况下关键性因素包括函数体的大小是否有局部对象被声明函数的复杂性等等。 2那么如果一个函数被声明为inline但是却没有被内联将会发生什么呢理论上当编译器拒绝内联一个函数的时候那个函数会像普通函数一样被对待但是还会出现一些其他的问题。例如下面这段代码 // filename Time.h #includectime #includeiostream using namespace std; class Time { public: inline void Show() { for (int i 0; i 10; i) couttime(0)endl;} }; 因为成员函数Time::Show()包括一个局部变量和一个for循环所以编译器一般拒绝inline并且把它当作一个普通的成员函数。但是这个包含类声明的头文件会被单独的#include进各个独立的编译单元中 // filename f1.cpp #include Time.hj void f1() { Time t1; t1.Show(); }
// filename f2.cpp #include Time.h void f2() { Time t2; t2.Show(); } 结果编译器为这个程序生成了两个相同成员函数的拷贝 void f1(); void f2(); int main() { f1(); f2(); return 0; } 当程序被链接的时候linker将会面对两个相同的Time::Show()拷贝于是函数重定义的连接错误发生。但是老一些的C实现对付这种情况的办法是通过把一个un-inlined函数当作static来处理。因此每一份函数拷贝仅仅在自己的编译单元中可见这样链接错误就解决了但是在程序中却会留下多份函数拷贝。在这种情况下程序的性能不但没有提升反而增加了编译和链接时间以及最终可执行体的大小。 但是幸运的是新的C标准中关于un-inlined函数的说法已经改变。一个符合标准C实现应该只生成一份函数拷贝。然而要想所有的编译器都支持这一点可能还需要很长时间。 另外关于内联函数还有两个更令人头疼的问题。第一个问题是该如何进行维护。一个函数开始的时候可能以内联的形式出现但是随着系统的扩展函数体可能要求添加额外的功能结果内联函数就变得不太可能因此需要把inline specifier去除以及把函数体放到一个单独的源文件中。另一个问题是当内联函数被应用在代码库的时候产生。当内联函数改变的时候用户必须重新编译他们的代码以反映这种改变。然而对于一个非内联函数用户仅仅需要重新链接就可以了。 这里想要说的是内联函数并不是一个增强性能的灵丹妙药。只有当函数非常短小的时候它才能得到我们想要的效果但是如果函数并不是很短而且在很多地方都被调用的话那么将会使得可执行体的体积增大。最令人烦恼的还是当编译器拒绝内联的时候。在老的实现中结果很不尽人意虽然在新的实现中有很大的改善但是仍然还是不那么完善的。一些编译器能够足够的聪明来指出哪些函数可以内联哪些不能但是大多数编译器就不那么聪明了因此这就需要我们的经验来判断。如果内联函数不能增强行能就避免使用它
四. 优化你的内存使用 通常优化都有几个方面更快的运行速度有效的系统资源使用更小的内存使用。一般情况下代码优化都是试图在以上各个方面进行改善。重新放置声明技术被证明是消除多余对象的建立和销毁这样既减小了程序的大小又加快了运行速度。然而其他的优化技术都是基于一个方面------更快的速度或者是更小的内存使用。有时这些目标是互斥的压缩了内存的使用往往却减慢了代码速度快速的代码却又需要更多的内存支持。下面总结两种在内存使用上的优化方法 1 Bit Fields 在C/C中都可以存取和访问数据的最小组成单元bit。因为bit并不是C/C基本的存取单元所以这里是通过牺牲运行速度来减少内存和辅助存储器的空间的使用。注意一些硬件结构可能提供了特殊的处理器指令来存取bit因此bit fields是否影响程序的速度取决于具体平台。 在我们的现实生活中一个数据的许多位都被浪费了因为某些应用根本就不会有那么大的数据范围。也许你会说bit是如此之小通过它就能减小存储空间的使用吗的确在数据量很小的情况下不会看出什么效果但是在数据量惊人的情况下它所节省的空间还是能够让我们的眼睛为之一亮的。也许你又会说现在内存和硬盘越来越便宜何苦要费半天劲这省不了几个钱。但是还有另外一个原因一定会使你信服那就是数字信息传输。一个分布式数据库都会在不同的地点有多份拷贝。那么数百万的纪录传输就会显得十分昂贵。Ok现在我们就来看看该如何做吧首先看下面这段代码 struct BillingRec { long cust_id; long timestamp; enum CallType { toll_free, local, regional, long_distance, international, cellular } type; enum CallTariff { off_peak, medium_rate, peak_time } tariff; }; 上面这个结构体在32位的机器上将会占用16字节你会发现其中有许多位都被浪费了尤其是那两个enum型浪费更是严重所以请看下面做出的改进 struct BillingRec { int cust_id: 24; // 23 bits 1 sign bit int timestamp: 24; enum CallType {//... }; enum CallTariff {//... }; unsigned call: 3; unsigned tariff: 2; }; 现在一个数据从16字节缩减到了8字节减少了一半怎么样效果还是显著的吧 2 Unions Unions通过把两个或更多的数据成员放置在相同地址的内存中来减少内存浪费这就要求在任何时间只能有一个数据成员有效。Union 可以有成员函数包括构造函数和析构函数但是它不能有虚函数。C支持anonymous unions。anonymous union是一个未命名类型的未命名对象。例如 union { long n; void * p}; // anonymous n 1000L; // members are directly accessed p 0; // n is now also 0 不像命名的union它不能有成员函数以及非public的数据成员。 那么unions什么时候是有用的呢下面这个类从数据库中获取一个人的信息。关键字既可以是一个特有的ID或者人名但是二者却不能同时有效 class PersonalDetails { private: char * name; long ID; //... public: PersonalDetails(const char *nm); //key is of type char * used PersonalDetails(long id) : ID(id) {} //numeric key used }; 上面这段代码中就会造成内存的浪费因为在一个时间只能有一个关键字有效。anonymous union可以在这里使用来减少内存的使用例如 class PersonalDetails { private: union //anonymous { char * name; long ID; }; public: PersonalDetails(const char *nm); PersonalDetails(long id) : ID(id) {/**/} // direct access to a member //... }; 通过使用unionPersonalDetails类的大小被减半。但是这里要说明的是节省4 个字节内存并不值得引入union所带来的麻烦除非这个类作为数百万数据库记录的类型或者纪录在一条很慢的通信线路传输。值得注意的是unions并不引入任何运行期负担所以这里不会有什么速度上的损失。anonymous union的优点就是它的成员可以被直接访问。
五. 速度优化 在一些对速度要求非常苛刻的应用系统中每一个CPU周期都是要争取的。这个部分展现了一些简单方法来进行速度优化。 1 使用类来包裹长的参数列表 一个函数调用的负担将会随着参数列表的增长而增加。运行时系统不得不建立堆栈来存储参数值通常当参数很多的时候这样一个操作就会花费很长的时间。 把参数列表包裹进一个单独的类中并且通过引用进行传递这样将会节省很多的时间。当然如果函数本身就很长那么建立堆栈的时间就可以忽略了因此也就没有必要这样做。然而对于那些执行时间很短而且经常被调用的函数来说包裹一个长的参数列表在对象中并且通过引用传递将会提高性能。 2 寄存器变量 register specifier被用来告诉编译器一个对象将被会非常多的使用可以把它放入寄存器中。例如 void f() { int *p new int[3000000]; register int *p2 p; //store the address in a register for (register int j 0; j 3000000; j) { *p2 0; } //...use p delete [] p; } 循环计数是应用寄存器变量的最好的候选者。当它们没有被存入一个寄存器中大部分的循环时间都被用在了从内存中取出变量和给变量赋新值上。如果把它存入一个寄存器中的话将会大大减少这种负担。需要注意的是register specifier仅仅是对编译器的一个建议。就好比内联函数一样编译器可以拒绝把一个对象存储到寄存器中。另外现代的编译器都会通过把变量放入寄存器中来优化循环计数。Register storage specifier并不仅仅局限在基本类型上它能够被应用于任何类型的对象。如果对象太大而不能装进寄存器的话编译器仍然能够把它放入一个高速存储器中例如cache。 用register storage specifier声明函数型参将会是建议编译器把实参存入寄存器中而不是堆栈中。例如
void f(register int j, register Date d);
3 把那些保持不变的对象声明为const 通过把对象声明为const编译器就可以利用这个声明把这样一个对象放入寄存器中。 4 Virtual function的运行期负担 当调用一个virtual function如果编译器能够解决调用的静态化将不会引入额外的负担。另外一个非常短的虚函数可以被内联处理。在下面这个例子中一个聪明的编译器能够做到静态调用虚函数 #include iostream using namespace std; class V { public: virtual void show() const { cout Im Vendl; } }; class W : public V { public: void show() const { cout Im Wendl; } }; void f(V v, V *pV) { v.show(); pV- show(); } void g() { V v; f(v, v); } int main() { g(); return 0; } 如果整个程序出现在一个单独的编译单元中编译器能够对main()中的g()进行内联替换。并且在g()中f()的调用也能够被内联处理。因为传给f()的参数的动态类型能够在编译期被知晓因此编译器能够把对虚函数的调用静态化。但是不能保证每个编译器都这样做。然而一些编译器确实能够利用在编译期获得参数的动态类型从而使得函数的调用在编译期间就确定了下来避免了动态绑定的负担。 5 Function objects VS function pointers 用function objects取代function pointers的好处不仅仅局限在能够泛化和简单的维护性上。而且编译器能够对function object的函数调用进行内联处理从而进一步的增强了性能
六. 最后的求助 迄今为止为大家展示的优化技术并没有在设计以及代码的可读性上做出妥协。事实上它们中的一些还提高了软件的稳固性和可维护性。但是在一些对时间和内存有严格限制的软件开发中上面的技术可能还不够有可能还需要一些会影响软件的可移植性和扩展性的技术。但是这些技术只能在所有其他的优化技术都被应用但是还不符合要求的情况下使用。 1 关闭RTTI和异常处理支持 当你导入纯C代码给C编译器的时候你可能会发现有一些性能上的损失。这并不是语言或者编译器的错误而是编译器作出的一些调整。如果你想获得和C编译器同样的性能那么请关闭编译器对RTTI以及异常处理的支持。为什么会这样呢因为为了支持RTTI和异常处理C编译器会插入额外的代码。这样就增加了可执行体的大小从而使得效率有所下降。当应用纯C代码的时候那些额外的代码是不需要的所以你可以通过关闭来避免它。 2 内联汇编 对时间要求苛刻的部分可以用本地汇编来重写。结果可能是速度上的显著提高。然而这个方法不能想当然的就去实施因为它将使得将来的修改非常的困难。维护代码的程序员可能对汇编并不了解。如果想要把软件运行于其他平台也需要重写汇编代码部分。另外开发和测试汇编代码是一件辛苦的工作它将花费更长的时间。 3 直接和操作系统进行交互 API函数可以使你直接与操作系统进行交互。有时直接执行一个系统命令可能会快许多。出于这个目的你可以使用标准函数system()。例如在一个dos/windows系统下你可以这样显示当前目录下的文件 #include cstdlib using namespace std; int main() { system( dir); //execute the dir command } 注意这里是在速度和可移植性以及可扩展性之间做出的折衷。 5.
代码优化 所谓代码优化是指对程序代码进行等价指不改变程序的运行结果变换。程序代码可以是中间代码如四元式代码也可以是目标代码。等价的含义是使得变换后的代码运行结果与变换前代码运行结果相同。优化的含义是最终生成的目标代码短运行时间更短、占用空间更小时空效率优化。原则上优化可以再编译的各个阶段进行但最主要的一类是对中间代码进行优化这类优化不依赖于具体的计算机。 目录 分类要点 编辑本段分类 编译过程中可进行的优化可按阶段划分优化可在编译的不同阶段进行分为中间代码一级和目标代码一级的优化。可按优化涉及的程序范围划分对同一阶段分为局部优化,循环优化和全局优化. 进行优化所需要的基础是对代码进行数据流分析和控制流分析。如划分
DAG查找循环分析变量的定值点和引用点等等。最常用的代码优化技术有删除多余运算循环不变代码外提强度削弱变换循环控制条件合并已知量与复写传播以及删除无用赋值等等。
编辑本段要点 一. 尽量采用
divcss布局您的页面divcss布局的好处是让
搜索引擎爬虫能够更顺利的更快的更友好的爬完您的页面;divcss布局还可以大量缩减网页大小使得代码更简洁流畅更容易放置更多内容。 二. 尽量缩减您的页面大小因为搜索引擎爬虫每次爬行您的站点时存储数据的容量有限一般建议100
KB以下越小越好但不能小于5KB。网页大小减少还有一个好处能够促使您的站点形成巨大的内部链接网。 三. 尽量少用无用的图片和flash。内容索引所派出的搜索引擎爬虫不认识图片只能根据图片“
ALT,
TITLE”等属性的内容判断图片的内容。对于
flash搜索引擎爬虫更是视而不见。 四. 尽量满足w3c标准网页代码的编写满足W3C标准能够提升网站和搜索引擎的友好度因为搜索引擎收录标准排名算法都是在
W3C标准的基础上开发的。 五. 尽量更深层次套用标签
h1、h2、h3、h4、h5…..让搜索引擎能够分辨清晰网页那一块很重要那一块次之。 六. 尽量少用
JSJS代码全部用外部调用文件封装。搜索引擎不喜欢JS影响网站的友好度指数。 七. 尽量不使用表格布局因为搜索引擎对表格布局嵌套3层以内的内容懒的去抓取。搜索引擎爬虫有时候也是比较懒的望各位一定要保持代码和内容在3层以内。 八. 尽量不让CSS分散在HTML标记里尽量封装到外部调用文件。如果CSS出现在
HTML标记里搜索引擎爬虫就要分散注意力去关注这些对优化没有任何意义的东西所以建议封装到专用CSS文件中。 九.清理垃圾代码要把代码编辑环境下敲击键盘上的空格键所产生的符号把一些默认属性代码不会影响显示的代码注释语句如果对代码可读性没有太大影响清理这些垃圾代码会减少不少的空间。