还剩29页未读,继续阅读
本资源只提供10页预览,全部文档请下载后查看!喜欢就下载吧,查找使用更方便
文本内容:
使用gcc和glibc来优化程序转载使用gcc和glibc来优化程序转载2011-01-121738Optimize Applicationswith gccand glibcby UlrichDrepper
1.介绍===本文总结一些关于代码优化的经验,这些经验是不完整的.本文不是讨论编译器如何优化代码,后者是完全不同的另外一个领域.
2.编译时优化Using OptimizationsPerformed atCompile-Time=====
2.1消除无用代码Dead CodeElimination DeadCode指永远不会执行的代码.例如long int addlong int a,void*ptr,int type{iftype==0return a+*int*ptr;else return a+*long int*ptr;}这个函数根据type的值来判断ptr的类型,从而求和.优化1多数情况下int和long int是相同的,因此可以优化为long int addlong int a,void*ptr,int type{ifsizeofint==sizeoflong int||type==0return a+*int*ptr;else return a+*long int*ptr;}sizeof运算总是在编译时进行,因此增加的条件表达式总是在编译时计算.如果long int和int确实相同,那么这个函数就可以被编译器优化.进一步优化,利用limits.h中定义的宏#include limits.h long intaddlong inta,void*ptr,int type{#if LONG_MAX!=INT_MAX iftype==0return a+*int*ptr;else#endif returna+*long int*ptr;}这样,即便在longint不同于int的平台上,该函数也被优化了
2.2节省函数调用Saving FunctionCalls很多函数很短小,相对函数执行的时间,函数调用的代价不可忽视.例如标准库中的字符串函数和数学函数.解决办法有两个使用宏代替函数,或者用inline函数.一般而言,inline函数和宏一样快,但是更安全.但是如果用到alloca和__builtin_constant_p的时候,可能要考虑用优先使用宏了但是,如果函数被声明为extern,inline并不总是有效了.另外,当gcc的编译优化选项没有打开时,gcc不会展开inline函数.如果inline函数是static的,那么编译器总是会展开该函数,不考虑是否真的值得.尤其是当使用-Osoptimize forspace选项时,static inline函数是否值得使用就是个问题了.编写正确而又安全的宏并不容易.要注意a正确使用括号括起参数,例如#define multa,ba*b//错误#define multa,ba*b b宏定义中的大括号引入新的block,这有时侯会导致问题.例如#define scaleresult,a,b,c\{\int c__=c;\*result=a*c__+b*c__;\}下面的代码编译会出现问题if.scaler,a,b,c;///多余的分号导致编译错误else else{}正确的写法应该是#define scaleresult,a,b,c\do{\int c__=c;\*result=a*c__+b*c__;\}while0c如果参数是表达式并且在宏定义中出现多次,尽量避免重复计算.这也是上面例子中要引入变量c__的原因.但这会限制变量c__的类型.d宏缺乏返回值
2.3编译器内部函数Compiler Intrinsics绝大部分C编译器都知道内部函数Intrinsic functions.它们是特殊的inline函数,由编译器提供使用.这些函数用外部实现来代替.gcc
2.96的内部函数有*__builtin_alloca动态分配栈上内存dynamiclly allocatememory onthe stack*__builtin_ffsfind firstbit set*__builtin_abs,__builtin_labsabsolute value of aninteger*__builtin_fabs,__builtin_fabsf,__builtin_fabsl absolutevalueof floating-point vlaue*__builtin_memcpy copymemory region*__builtin_memset setmemory regionto givevalue*__builtin_memcmp comparememory region*__builtin_strcmp*__builtin_strcpy*__builtin_strlen*__builtin_sqrt,__builtin_sqrtf,__builtin_sqrtl*__builtin_sin,__builtin_sinf,__builtin_sinl*__builtin_cos,__builtin_cosf,__builtin_cosl*__builtin_div,__builtin_ldiv integerdivision withrest*__builtin_fmod,__builtin_frem moduleand remainderof floating-point value不能保证所有内部函数在所有平台上都定义了.关于intrinsic function,有一个很有用的特性如果参数在编译时是常数,那么可以在编译时计算其值.例如strlenfoo bar有可能在编译时就计算好.
2.4__builtin_constant_p__builtin_constant_p并不属于intrinsic function,它是一个类似于sizeof的操作符.__builtin_constant_p接收一个参数,如果该参数在运行时是固定不变的constant atruntime,那么就返回非0值,表示这是一个常量.例如,前面的add函数可以在进一步优化#define adda,ptr,type\__extension__\__buildtin_constant_ptype\a+type==0\*int*ptr\*longint*ptr\adda,ptr,type如果第三个参数为constant,那么这个宏将改变add函数的行为;否则就调用真正的add函数.这样尽量在编译时计算,从而提高了效率.
2.5type-generic macro有时侯我们希望宏对不同的参数数据类型,能正确处理不同数据类型并表现相同的行为,可以借助__typeof__例如前面的scale#define tgscaleresult,a,b,c\do{\__externsion____typeof__a+b+cc__=c;\*result=a*c__+b*c__;\}while0这里,c__自动拥有返回值类型,而不是前面固定写的int类型.__typeof__o定义了与o相同的类型.__typeof__的另外一个用途被ISO C9x用于tgmath中,从而实现一些对任意数据类型包括复数都适用的数学函数.错误示例#define sinval\sizeof__real__valsizeofdouble\sizeof__real__val==sizeofval\sinlval csinlval\sizeof__real__val==sizeofdouble\sizeof__real__val==sizeofval\sinval csinval\sizeof__real__val==sizeofval\sinfval csinfval上面这个宏的意思是如果val是虚数即sizeof__real__val!=sizeofval,那么对val调用csinl,csin和csinf如果val是实数,且比double精度高,即sizeof__real__valsizeofdouble,那么对val调用sinl,就long double,否则调用sin或者sinf.sinl相当于sinlong doublesin相当于sindouble sinf相当于sinfloat csin对应的复数sin函数但是这个宏是有错误的,由于整个宏是一个表达式,表达式是有静态的类型的,能代表该表达式的数据类型必须有足够的精度来表示各种值,所以这个表达式的最终数据类型就是comple longdouble,这并不是我们期望的.正确的实现方法是#define sinval\__extension__\{__typeof__val__tgmres;\ifsizeof__real__valsizeofdouble\{\ifsizeof__real__val==sizeofval\__tgmres=sinlval;\else\__tgmres=csinlval;\}\else ifsizeof__real__val==sizeofdouble\{\ifsizeof__real__val==sizeofval\__tgmres=sinval;\else\__tgmres=csinval;\}\else\{\ifsizeof__real__val==sizeofval\__tgmres=sinfval;\else\__tgmres=csinfval;\}\__tgmres;}上面对__tgmres赋值的6个分支中,真正会执行的那个分支是不存在精度损失的;其他分支都会作为deadcode被编译器优化掉
3.help thecompiler==GNU C编译器提供一些扩展来更清晰的描述程序,从而帮助编译器生成代码.
3.1不返回的函数Functions ofNo Return大项目一般都至少有一个用于严重错误处理的函数,这个函数体面的结束应用程序.这个函数一般情况下不会被编译器优化,因为编译器不知道它不返回.例如void fatal.__attribute____noreturn__;void fatal.{//print somemessage exit1;}//application code{ifd==0fatal.;else a=b/d;}函数fatal保证不会返回,exit函数也不返回.因此可以在函数原型上加上__attribute____noreturn__.如果没有noreturn的标记,gcc会把上面的代码翻译成下面的形式伪代码1compare dwithzero2if notzero jump to53call fatal4jumpto65compute b/d andassign toa
6.如果有noreturn标记,gcc可以优化代码,省略
4.对应的源代码为{ifd==0fatal.;a=b/d;}
3.2常值函数constant valuefunctions有些函数的值仅仅取决于传入的参数,这种函数没有副作用,我们称之为pure function.对于相同的参数,这种函数有相同的返回值.举例说明htons函数要么返回参数如果是big-endian计算机,要么交换字节顺序如果计算机是little-endian.这个函数没有副作用,是一个pure function.那么下面的代码可以被优化{short intserver=.while1{struct sockaddr_in s_in;memsets_in,0,sizeof s_in;s_in.sin_port=htonsserv;.}}优化后的结果为{short intserver=.serv=htonsserv;while1{struct sockaddr_in s_in;memsets_in,0,sizeof s_in;s_in.sin_port=serv;.}}从而减少循环中执行的代码,节省CPU.但是编译器并无法知道函数是否是pure function.我们必须给pure function显著的标记extern uint16_t htonsuint16_t__x__attribute____const__;__const__可以用来标记pure function.
3.3Different CallingConventions每种平台都支持特定的calling conventions以便由不同语言和编译器写的程序/库能够一起工作.但是,有时侯在某些平台上,编译器支持一种更高效的calling convention.在项目内部使用这种calling convention不会影响系统的其他部分.尤其是在Intel ia32平台上,编译器支持多种不同于标准Unix x86的calling convention,这有时侯会大大提高程序速度.GNU C编译器手册有更详细解释.本节只讨论x86平台.改变函数的calling convention的两个办法1命令行选项command lineoption这种方法不安全,所有函数包括exported function都受到影响2对单个函数设置function attribute.
3.
3.1__stdcall__一般情况下,函数参数是通过栈来传递的,因此需要在某个位置调整栈指针.ia32unix平台上标准的calling convention是让调用方caller调整栈指针;因此可以延迟调整操作,一次同时调整多个函数的栈指针.如果函数被标记为__stdcall__,这意味这个函数自己调整栈指针.在ia32平台上,这不算是坏注意,因为ia32体系结构提供一个指令,能同时从函数调用返回并调整栈指针.示例int__attribute____stdcall__addint a,int b{returna+b;}int foointa{return adda,42;}int barvoid{return foo100;}上面的代码翻译成汇编大致如下8add900008B442408movl8%esp,%eax10000403442404addl4%esp,%eax110008C20800ret.17foo1800106A2A pushl190012FF742408pushl8%esp200016E8E5FFFF calladd20FF21001b C3ret.27bar2800206A64pushl0290022E8E9FFFF callfoo29FF30002783C404addl,%esp31002a C3ret从上面的例子可以看出,add函数被标记为__stdcall__,foo函数在调用add后直接返回,不需要调整栈指针,因为add函数已经调整来指针ret指令完成返回和调整指针操作;而bar函数调用foo函数,调用结束后必须调整栈指针.由此可见,使用__stdcall__是有好处的;但是,现代编译器都已经很智能,能作到一次性为多个函数调用调整栈指针,从而使得生成的代码更少速度更快.此外,以后的发展可能会出现更快的调用方式,所以使用__stdcall__必须非常谨慎.
3.
3.2__regparm____regparm__只能在ia32平台上使用,它能指明有多少个最多3个整数和指针参数是通过寄存器来传递的,而不是通过栈传递.当函数体比较短小,而且参数立刻就能使用时,这种方式效果很显著.假设有下面的例子int__attribute____regparm__3addint a,int b{returna+b;}经过编译优化后,生成的代码时8add9000001D0addl%edx,%eax100002C3ret这个代码比起
3.
3.1中add的代码更高效.用寄存器传参数总是很快.
3.4Sibling Calls经常有这样的代码一个函数最后结束时是在调用另外一个函数.这种情况下生成的伪代码如下//this isin functionf1ncall functionf2n+1execute codeof f2n+2get returnaddress from call inf1n+3jump backinto functionf1n+4optionally adjuststack pinterfromcall to f2n+5get returnaddress fromcalltof1n+6jump backto calleroff1经过优化,f1在调用f2结束后可以直接返回.
3.5使用goto goto有时侯提高效率
4.了解库Knowing theLibraries==
4.1strcpy vs.memcpy strcpy两个参数src和dest,逐个byte拷贝memcpy三个参数,src,dest和size,按word拷贝strncpy3个参数src,dest和length退出条件遇到NUL字符或达到拷贝长度逐个检查byte是否为NUL追加NUL字符非gcc内部函数memcpy3个参数退出条件达到拷贝长度按word检查长度不必追加NUL字符gcc内部函数,特殊优化类似的,mem*和对应的str*函数都存在差别.mem*函数参数多些,一般情况下这不是问题,可以通过寄存器传参数;但是当函数被inline的时候,寄存器可能不够,生成的代码可能稍微复杂一些.建议如下*尽量别使用strncpy,而使用strcpy*如果要拷贝的字符串很短,用strcpy*如果字符串可能很长,用memcpy
4.2strcat和strncat关于字符串操作的一个金口玉言gold rule是绝对不要使用strcat和strncat.要使用这两个函数,必须知道长度,并准备足够的空间.定型代码如下{char*buf=.;size_t bufmax=.;ifstrlenbuf+strlens+1bufmax buf=char*reallocbuf,bufmax*=2;strcatbuf,s;}上面的代码中,已经调用了strlen,strcat中会重复执行strlen操作,因此更高效的作法是{char*buf=.;size_t bufmax=.;size_t slen;size_t buflen;slen=strlens+1;buflen=strlenbuf;ifbuflen+slen bufmaxbuf=char*reallocbuf,bufmax*=2;memcpybuf+buflen,s,slen;}
4.3内存分配malloc和calloc分配堆内存.alloca分配栈内存.malloc的实现从内核申请内存,可能会调用sbrk系统调用;在某些系统上如果申请的内存很多,可能会调用mmap来分配内存.malloc的内部实现会用相关的数据结构来管理好申请内存,以便释放或者重新申请.因此调用malloc的代价并不低.alloca的实现相对简单得多,起码编译器能直接把它作为inline来编译,alloca只是简单修改一下栈指针就可以了.而且,调用alloca后不需要调用free函数来释放内存.free函数的代价也是不小的.但是,alloca申请的内存只能用在当前函数中,而且alloca不适合用来申请大量内存,很多平台系统出于安全考虑对栈的大小有限制.malloc的实现和内核相关,能更好的处理大内存申请.alloca总是成功的,因为它只是执行修改栈指针操作而已.因此alloca非常适合在函数内部申请局部使用的内存,不比检查申请释放成功,也不必调用free来释放内存,不仅提高性能还简化来代码.示例如下int tmpcopyconstint*a,inta{int*tmp=int*mallocn*sizeofint;int_fast32_t count;int result;iftmp==NULL return-1;forcount=0;count n;++count tmp[count]=a[count]^0xffffffff;result=footmp,n;freetmp;return result;}用alloca改良后的代码变简单了省略了free和指针检查.int tmpcopyconstint*a,inta{int*tmp=int*allocan*sizeofint;int_fast32_t count;forcount=0;count n;++count tmp[count]=a[count]^0xffffffff;return footmp,n;}GNU libc提供strdupa和strndupa,就是用来局部临时拷贝字符串.它们与strdup和strndup的不同就是,前者调用alloca,后者调用malloc.因此strdupa和strndupa只能是宏,而不能是函数.下面是strdupa的错误实现//Please notethis isWRONG!#define strdupas\__extension__\{\__const char*__old=s;\size_t__len=strlen__old+1;\char*memcpy__builtin_alloca__len,__old,__len;\}上面的实现代码中,memcpy对alloca的调用是错误的,因为alloca修改了栈指针,而在某些系统上,传递给函数调用的参数也是放在栈上的,这会导致严重错误.所以,绝对不能在函数参数列表中调用alloca,也不能在参数列表中通过strdupa或其他方式隐式调用alloca.上面的代码被编译成这样的伪代码
1.push__len onthe stack,change stack pointer
2.push__old inthe stack,change stackpointer
3.modify stackpointer fornewly allocatedobject
4.push currentstackpointeron stack,change stackpointer
5.call memcpy正确代码应该是这样的//Duplicate S,returning anidentical allocadstring.#define strdupas\__extension__\{\__const char*__old=s;\size_t__len=strlen__old+1;\char*__new=char*__builtin_alloca__len;\char*memcpy__new,__old,__len;\}
4.4其他内存相关问题1realloc代价相当高,要执行malloc/memcpy/free三个操作2malloc并不一定每次都向系统内核申请内存,它本身也管理了内存的申请和释放;释放的内存不一定还给系统,而是留给下次申请3估算出程序需要的大致内存,减少申请和分配次数,可以提高效率4ISO C定义了函数calloc,这个函数分配内存会全部用0填充,它比调用malloc和memset更高效,因为对于通过内核mmap得到内存已经被清0了,calloc不会再执行清0操作;而对sbrk获得的内存,calloc会执行清0操作;从而节省不必要的清0操作.
4.5用最合适的数据类型ISO C9x定义了一个重要的新头文件stdint.h.其中定义了int_least8_t,uint_least8_t,int_least16_t,.int_least64_t等数据类型.int8_t确保正好有8bit;而int_least8_t保证最少有8bit,少于8bit的整数可以安全存放再int_least8_t中.例如,有个整数数组,每个元素包含16bit的值,并且被频繁使用.如果我们将其定义为int_least16_t,那么在有些平台上可能会分配64bit,从而更快的访问数组元素,提高性能.类似的,假设有下面的代码{short intn;.forn=0;n500;++n c[n]=a[n]+b[n];}循环阀值500用16bit表示足够,可以将循环计数器定义为int_fast16_t类型,编译器能识别该类型,可能将它存放在寄存器中,从而提高性能.
4.6非标准字符串函数经常会有获得字符串结尾位置指针的需求,最直接的作法是char*s=.;s+=strlens;这种方法执行了多余的+操作,strlen其实已经遍历到结尾了.方法2char*s=.s=strchrs,[message];strchr直接返回了末尾指针位置,但是这种作法增加了strchr比较运算.方法3非标准函数rawmemchrconst char*,char,它在memchr的基础上减少了size参数,从而减少了比较运算,提高了效率.
5.Write BetterCode===
5.1正确编写和使用库函数假设要实现strdup函数实现1char*duplicateconst char*s{char*res=xmallocstrlens+1;strcpyres,s;return res;}其中xmallc是GNU提供的failesafe的malloc实现.改进实现2用memcpy代替strcpy char*duplicate2const char*s{size_t len=strlens+1;char*res=xmalloclen+1;memcpyres,s,len;return res;}改进实现3直接返回memcpy的返回值.memcpy是有返回值的.char*duplicate3const char*s{size_t len=strlens+1;return memcpyxmalloclen,s,len;}改进实现4编译时优化#define duplicate4s\__buildtin_constant_ps\duplicate_cs,strlens+1\duplicate3s char*duplicate_cconst char*s,size_t len{returnchar*memcpyxmalloclen,s,len;}
5.2Computed goto有些函数由于设计或者性能的原因,难以分割成若干个小函数,导致函数有许多条件分支,从而降低执行性能.解决办法就是使用状态机.实现状态机的最简单方法就是使用switch.另外一个办法就是使用状态跳转表goto.示例如下{.switch*cp{caselislong=1;++cp;break;casehisshort=1;++cp;break;default}switch*cp{cased.break;caseh.break;default}}使用状态跳转表{static constvoid*jumps1={[l]=do_l,[h]=do_h,[d]=do_d,[g]=do_g};static constvoid*jumps2={[d]=do_d,[g]=do_g};goto*jmps1[*cp];do_lislong=1;++cp;goto jumps2[*cp];do_hisshort=1;++cp;goto jumps2[*cp];do_d.goto out;do_g.goto out;out}
6.Profiling==对程序进行profile分两种1基于时间找出最消耗时间的代码2基于调用关系找出函数调用次数和调用关系有些函数很小巧,可能被调用次数很多,但执行时间并不多.
6.1grof profilinggcc编译程序的时候加上-pg选项,gcc-c foo.c-o foo.o-pg link的时候加上-profile选项gcc-o foofoo.o-profile
6.2sprof profiling不需要重新编译,只需设置环境LD_PROFILE=libc.so.6LD_PROFILE_OUTPUT=.然后运行程序,可以检查对库的调用.特别声明1资料来源于互联网,版权归属原作者2资料内容属于网络意见,与本账号立场无关3如有侵权,请告知,立即删除。