为了正常的体验网站,请在浏览器设置里面开启Javascript功能!

使用gcc和glibc来优化程序 转载

2022-04-03 7页 doc 171KB 2阅读

用户头像 个人认证

is_780376

暂无简介

举报
使用gcc和glibc来优化程序 转载Goodisgood,butbettercarriesit.精益求精,善益求善。使用gcc和glibc来优化程序转载使用gcc和glibc来优化程序转载使用gcc和glibc来优化程序转载PAGEPAGE25使用gcc和glibc来优化程序转载PAGE使用gcc和glibc来优化程序转载使用gcc和glibc来优化程序(转载)2011-01-1217:38OptimizeApplicationswithgccandglibcbyUlrichDrepper1.介绍===本文总结一些关于代码优化的经验,这些经验是...
使用gcc和glibc来优化程序  转载
Goodisgood,butbettercarriesit.精益求精,善益求善。使用gcc和glibc来优化程序转载使用gcc和glibc来优化程序转载使用gcc和glibc来优化程序转载PAGEPAGE25使用gcc和glibc来优化程序转载PAGE使用gcc和glibc来优化程序转载使用gcc和glibc来优化程序(转载)2011-01-1217:38OptimizeApplicationswithgccandglibcbyUlrichDrepper1.介绍===本文总结一些关于代码优化的经验,这些经验是不完整的.本文不是讨论编译器如何优化代码,后者是完全不同的另外一个领域.2.编译时优化(UsingOptimizationsPerformedatCompile-Time)=====2.1消除无用代码(DeadCodeElimination)DeadCode指永远不会执行的代码.例如:longintadd(longinta,void*ptr,inttype){if(type==0)returna+*(int*)ptr;elsereturna+*(longint*)ptr;}这个函数根据type的值来判断ptr的类型,从而求和.优化1:多数情况下int和longint是相同的,因此可以优化为longintadd(longinta,void*ptr,inttype){if(sizeof(int)==sizeof(longint)||(type==0))returna+*(int*)ptr;elsereturna+*(longint*)ptr;}sizeof运算总是在编译时进行,因此增加的条件表达式总是在编译时计算.如果longint和int确实相同,那么这个函数就可以被编译器优化.进一步优化,利用limits.h中定义的宏#includelimits.hlongintadd(longinta,void*ptr,inttype){#ifLONG_MAX!=INT_MAXif(type==0)returna+*(int*)ptr;else#endifreturna+*(longint*)ptr;}这样,即便在longint不同于int的平台上,该函数也被优化了2.2节省函数调用(SavingFunctionCalls)很多函数很短小,相对函数执行的时间,函数调用的代价不可忽视.例如库中的字符串函数和数学函数.解决办法有两个:使用宏代替函数,或者用inline函数.一般而言,inline函数和宏一样快,但是更安全.但是如果用到alloca和__builtin_constant_p的时候,可能要考虑用优先使用宏了但是,如果函数被声明为extern,inline并不总是有效了.另外,当gcc的编译优化选项没有打开时,gcc不会展开inline函数.如果inline函数是static的,那么编译器总是会展开该函数,不考虑是否真的值得.尤其是当使用-Os(optimizeforspace)选项时,staticinline函数是否值得使用就是个问题了.编写正确而又安全的宏并不容易.要注意a)正确使用括号括起参数,例如#definemult(a,b)(a*b)//错误#definemult(a,b)((a)*(b))b)宏定义中的大括号引入新的block,这有时侯会导致问题.例如#definescale(result,a,b,c)\{\intc__=(c);\*(result)=(a)*c__+(b)*c__;\}下面的代码编译会出现问题:if(.)scale(r,a,b,c);///多余的分号导致编译错误elseelse{}正确的写法应该是:#definescale(result,a,b,c)\do{\intc__=(c);\*(result)=(a)*c__+(b)*c__;\}while(0)c)如果参数是表达式并且在宏定义中出现多次,尽量避免重复计算.这也是上面例子中要引入变量c__的原因.但这会限制变量c__的类型.d)宏缺乏返回值2.3编译器内部函数(CompilerIntrinsics)绝大部分C编译器都知道内部函数(Intrinsicfunctions).它们是特殊的inline函数,由编译器提供使用.这些函数用外部实现来代替.gcc2.96的内部函数有*__builtin_alloca:动态分配栈上内存dynamicllyallocatememoryonthestack*__builtin_ffs:findfirstbitset*__builtin_abs,__builtin_labs:absolutevalueofaninteger*__builtin_fabs,__builtin_fabsf,__builtin_fabslabsolutevalueoffloating-pointvlaue*__builtin_memcpycopymemoryregion*__builtin_memsetsetmemoryregiontogivevalue*__builtin_memcmpcomparememoryregion*__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_ldivintegerdivisionwithrest*__builtin_fmod,__builtin_fremmoduleandremainderoffloating-pointvalue不能保证所有内部函数在所有平台上都定义了.关于intrinsicfunction,有一个很有用的特性:如果参数在编译时是常数,那么可以在编译时计算其值.例如strlen("foobar")有可能在编译时就计算好.2.4__builtin_constant_p__builtin_constant_p并不属于intrinsicfunction,它是一个类似于sizeof的操作符.__builtin_constant_p接收一个参数,如果该参数在运行时是固定不变的(constantatruntime),那么就返回非0值,表示这是一个常量.例如,前面的add函数可以在进一步优化:#defineadd(a,ptr,type)\(__extension__\(__buildtin_constant_p(type)\?((a)+((type)==0\?*(int*)(ptr):\*(longint*)(ptr)))\:add(a,ptr,type)))如果第三个参数为constant,那么这个宏将改变add函数的行为;否则就调用真正的add函数.这样尽量在编译时计算,从而提高了效率.2.5type-genericmacro有时侯我们希望宏对不同的参数数据类型,能正确处理不同数据类型并表现相同的行为,可以借助__typeof__例如前面的scale#definetgscale(result,a,b,c)\do{\__externsion____typeof__((a)+(b)+(c))c__=(c);\*(result)=(a)*c__+(b)*c__;\}while(0)这里,c__自动拥有返回值类型,而不是前面固定写的int类型.__typeof__(o)定义了与o相同的类型.__typeof__的另外一个用途:被ISOC9x用于tgmath中,从而实现一些对任意数据类型(包括复数)都适用的数学函数.错误示例:#definesin(val)\(sizeof(__real__(val))sizeof(double)?\(sizeof(__real__(val))==sizeof(val)?\sinl(val):csinl(val))\:(sizeof(__real__(val))==sizeof(double)?\(sizeof(__real__(val))==sizeof(val)?\:sin(val):csin(val))\:(sizeof(__real__(val))==sizeof(val)?\sinf(val):csinf(val))))上面这个宏的意思是:如果val是虚数(即sizeof(__real__(val))!=sizeof(val)),那么对val调用csinl,csin和csinf如果val是实数,且比double精度高,即sizeof(__real__(val))sizeof(double)),那么对val调用sinl,就longdouble,否则调用sin或者sinf.sinl:相当于sin(longdouble)sin:相当于sin(double)sinf:相当于sin(float)csin:对应的复数sin函数但是这个宏是有错误的,由于整个宏是一个表达式,表达式是有静态的类型的,能代表该表达式的数据类型必须有足够的精度来表示各种值,所以这个表达式的最终数据类型就是complelongdouble,这并不是我们期望的.正确的实现方法是:#definesin(val)\(__extension__\({__typeof__(val)__tgmres;\if(sizeof(__real__(val))sizeof(double))\{\if(sizeof(__real__(val))==sizeof(val))\__tgmres=sinl(val);\else\__tgmres=csinl(val);\}\elseif(sizeof(__real__(val))==sizeof(double))\{\if(sizeof(__real__(val))==sizeof(val))\__tgmres=sin(val);\else\__tgmres=csin(val);\}\else\{\if(sizeof(__real__(val))==sizeof(val))\__tgmres=sinf(val);\else\__tgmres=csinf(val);\}\__tgmres;}))上面对__tgmres赋值的6个分支中,真正会执行的那个分支是不存在精度损失的;其他分支都会作为deadcode被编译器优化掉3.helpthecompiler==GNUC编译器提供一些扩展来更清晰的描述程序,从而帮助编译器生成代码.3.1不返回的函数(FunctionsofNoReturn)大项目一般都至少有一个用于严重错误处理的函数,这个函数体面的结束应用程序.这个函数一般情况下不会被编译器优化,因为编译器不知道它不返回.例如:voidfatal(.)__attribute__((__noreturn__));voidfatal(.){//printsomemessageexit(1);}//applicationcode{if(d==0)fatal(.);elsea=b/d;}函数fatal保证不会返回,exit函数也不返回.因此可以在函数原型上加上__attribute__((__noreturn__)).如果没有noreturn的标记,gcc会把上面的代码翻译成下面的形式(伪代码):1)comparedwithzero2)ifnotzerojumpto5)3)callfatal4)jumpto6)5)computeb/dandassigntoa6).如果有noreturn标记,gcc可以优化代码,省略4).对应的源代码为{if(d==0)fatal(.);a=b/d;}3.2常值函数(constantvaluefunctions)有些函数的值仅仅取决于传入的参数,这种函数没有副作用,我们称之为purefunction.对于相同的参数,这种函数有相同的返回值.举例说明:htons函数要么返回参数(如果是big-endian计算机),要么交换字节顺序(如果计算机是little-endian).这个函数没有副作用,是一个purefunction.那么下面的代码可以被优化:{shortintserver=.while(1){structsockaddr_ins_in;memset(&s_in,0,sizeofs_in);s_in.sin_port=htons(serv);.}}优化后的结果为:{shortintserver=.serv=htons(serv);while(1){structsockaddr_ins_in;memset(&s_in,0,sizeofs_in);s_in.sin_port=serv;.}}从而减少循环中执行的代码,节省CPU.但是编译器并无法知道函数是否是purefunction.我们必须给purefunction显著的标记:externuint16_thtons(uint16_t__x)__attribute__((__const__));__const__可以用来标记purefunction.3.3DifferentCallingConventions每种平台都支持特定的callingconventions以便由不同语言和编译器写的程序/库能够一起工作.但是,有时侯在某些平台上,编译器支持一种更高效的callingconvention.在项目内部使用这种callingconvention不会影响系统的其他部分.尤其是在Intelia32平台上,编译器支持多种不同于标准Unixx86的callingconvention,这有时侯会大大提高程序速度.GNUC编译器手册有更详细解释.本节只讨论x86平台.改变函数的callingconvention的两个办法:1)命令行选项(commandlineoption):这种方法不安全,所有函数(包括exportedfunction)都受到影响2)对单个函数设置functionattribute.3.3.1__stdcall__一般情况下,函数参数是通过栈来传递的,因此需要在某个位置调整栈指针.ia32unix平台上标准的callingconvention是让调用方(caller)调整栈指针;因此可以延迟调整操作,一次同时调整多个函数的栈指针.如果函数被标记为__stdcall__,这意味这个函数自己调整栈指针.在ia32平台上,这不算是坏注意,因为ia32体系结构提供一个指令,能同时从函数调用返回并调整栈指针.示例:int__attribute__((__stdcall__))add(inta,intb){returna+b;}intfoo(inta){returnadd(a,42);}intbar(void){returnfoo(100);}上面的代码翻译成汇编大致如下:8add:900008B442408movl8(%esp),%eax10000403442404addl4(%esp),%eax110008C20800ret.17foo:1800106A2Apushl190012FF742408pushl8(%esp)200016E8E5FFFFcalladd20FF21001bC3ret.27bar:2800206A64pushl0290022E8E9FFFFcallfoo29FF30002783C404addl,%esp31002aC3ret从上面的例子可以看出,add函数被标记为__stdcall__,foo函数在调用add后直接返回,不需要调整栈指针,因为add函数已经调整来指针(ret指令完成返回和调整指针操作);而bar函数调用foo函数,调用结束后必须调整栈指针.由此可见,使用__stdcall__是有好处的;但是,现代编译器都已经很智能,能作到一次性为多个函数调用调整栈指针,从而使得生成的代码更少速度更快.此外,以后的发展可能会出现更快的调用方式,所以使用__stdcall__必须非常谨慎.3.3.2__regparm____regparm__只能在ia32平台上使用,它能指明有多少个(最多3个)整数和指针参数是通过寄存器来传递的,而不是通过栈传递.当函数体比较短小,而且参数立刻就能使用时,这种方式效果很显著.假设有下面的例子:int__attribute__((__regparm__(3)))add(inta,intb){returna+b;}经过编译优化后,生成的代码时8add:9000001D0addl%edx,%eax100002C3ret这个代码比起3.3.1中add的代码更高效.用寄存器传参数总是很快.3.4SiblingCalls经常有这样的代码:一个函数最后结束时是在调用另外一个函数.这种情况下生成的伪代码如下://thisisinfunctionf1ncallfunctionf2n+1executecodeoff2n+2getreturnaddressfromcallinf1n+3jumpbackintofunctionf1n+4optionallyadjuststackpinterfromcalltof2n+5getreturnaddressfromcalltof1n+6jumpbacktocalleroff1经过优化,f1在调用f2结束后可以直接返回.3.5使用gotogoto有时侯提高效率4.了解库(KnowingtheLibraries)==4.1strcpyvs.memcpystrcpy:两个参数src和dest,逐个byte拷贝memcpy:三个参数,src,dest和size,按word拷贝strncpy:3个参数:src,dest和length退出条件:遇到NUL字符或达到拷贝长度逐个检查byte是否为NUL追加NUL字符非gcc内部函数memcpy:3个参数退出条件:达到拷贝长度按word检查长度不必追加NUL字符gcc内部函数,特殊优化类似的,mem*和对应的str*函数都存在差别.mem*函数参数多些,一般情况下这不是问题,可以通过寄存器传参数;但是当函数被inline的时候,寄存器可能不够,生成的代码可能稍微复杂一些.建议如下:*尽量别使用strncpy,而使用strcpy*如果要拷贝的字符串很短,用strcpy*如果字符串可能很长,用memcpy4.2strcat和strncat关于字符串操作的一个金口玉言(goldrule)是:绝对不要使用strcat和strncat.要使用这两个函数,必须知道长度,并准备足够的空间.定型代码如下:{char*buf=.;size_tbufmax=.;if(strlen(buf)+strlen(s)+1bufmax)buf=(char*)realloc(buf,(bufmax*=2));strcat(buf,s);}上面的代码中,已经调用了strlen,strcat中会重复执行strlen操作,因此更高效的作法是:{char*buf=.;size_tbufmax=.;size_tslen;size_tbuflen;slen=strlen(s)+1;buflen=strlen(buf);if(buflen+slenbufmax)buf=(char*)realloc(buf,(bufmax*=2));memcpy(buf+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来释放内存,不仅提高性能还简化来代码.示例如下:inttmpcopy(constint*a,inta){int*tmp=(int*)malloc(n*sizeof(int));int_fast32_tcount;intresult;if(tmp==NULL)return-1;for(count=0;countn;++count)tmp[count]=a[count]^0xffffffff;result=foo(tmp,n);free(tmp);returnresult;}用alloca改良后的代码变简单了:省略了free和指针检查.inttmpcopy(constint*a,inta){int*tmp=(int*)alloca(n*sizeof(int));int_fast32_tcount;for(count=0;countn;++count)tmp[count]=a[count]^0xffffffff;returnfoo(tmp,n);}GNUlibc提供strdupa和strndupa,就是用来局部临时拷贝字符串.它们与strdup和strndup的不同就是,前者调用alloca,后者调用malloc.因此strdupa和strndupa只能是宏,而不能是函数.下面是strdupa的错误实现://PleasenotethisisWRONG!#definestrdupa(s)\(__extension__\({\__constchar*__old=(s);\size_t__len=strlen(__old)+1;\(char*)memcpy(__builtin_alloca(__len),__old,__len);\}))上面的实现代码中,memcpy对alloca的调用是错误的,因为alloca修改了栈指针,而在某些系统上,传递给函数调用的参数也是放在栈上的,这会导致严重错误.所以,绝对不能在函数参数列表中调用alloca,也不能在参数列表中通过strdupa或其他方式隐式调用alloca.上面的代码被编译成这样的伪代码:1.push__lenonthestack,changestackpointer2.push__oldinthestack,changestackpointer3.modifystackpointerfornewlyallocatedobject4.pushcurrentstackpointeronstack,changestackpointer5.callmemcpy正确代码应该是这样的://DuplicateS,returninganidenticalalloca'dstring.#definestrdupa(s)\(__extension__\({\__constchar*__old=(s);\size_t__len=strlen(__old)+1;\char*__new=(char*)__builtin_alloca(__len);\(char*)memcpy(__new,__old,__len);\}))4.4其他内存相关问题1)realloc代价相当高,要执行malloc/memcpy/free三个操作2)malloc并不一定每次都向系统内核申请内存,它本身也管理了内存的申请和释放;释放的内存不一定还给系统,而是留给下次申请3)估算出程序需要的大致内存,减少申请和分配次数,可以提高效率4)ISOC定义了函数calloc,这个函数分配内存会全部用0填充,它比调用malloc和memset更高效,因为对于通过内核mmap得到内存已经被清0了,calloc不会再执行清0操作;而对sbrk获得的内存,calloc会执行清0操作;从而节省不必要的清0操作.4.5用最合适的数据类型ISOC9x定义了一个重要的新头文件: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,从而更快的访问数组元素,提高性能.类似的,假设有下面的代码:{shortintn;.for(n=0;n500;++n)c[n]=a[n]+b[n];}循环阀值500用16bit表示足够,可以将循环计数器定义为int_fast16_t类型,编译器能识别该类型,可能将它存放在寄存器中,从而提高性能.4.6非标准字符串函数经常会有获得字符串结尾位置指针的需求,最直接的作法是char*s=.;s+=strlen(s);这种方法执行了多余的+操作,strlen其实已经遍历到结尾了.方法2:char*s=.s=strchr(s,'[message]');strchr直接返回了末尾指针位置,但是这种作法增加了strchr比较运算.方法3:非标准函数rawmemchr(constchar*,char),它在memchr的基础上减少了size参数,从而减少了比较运算,提高了效率.5.WriteBetterCode===5.1正确编写和使用库函数假设要实现strdup函数:实现1:char*duplicate(constchar*s){char*res=xmalloc(strlen(s)+1);strcpy(res,s);returnres;}其中xmallc是GNU提供的failesafe的malloc实现.改进实现2:用memcpy代替strcpychar*duplicate2(constchar*s){size_tlen=strlen(s)+1;char*res=xmalloc(len)+1;memcpy(res,s,len);returnres;}改进实现3:直接返回memcpy的返回值.memcpy是有返回值的.char*duplicate3(constchar*s){size_tlen=strlen(s)+1;returnmemcpy(xmalloc(len),s,len);}改进实现4:编译时优化#defineduplicate4(s)\(__buildtin_constant_p(s)\?duplicate_c(s,strlen(s)+1)\:duplicate3(s))char*duplicate_c(constchar*s,size_tlen){return(char*)memcpy(xmalloc(len),s,len);}5.2Computedgoto有些函数由于设计或者性能的原因,难以分割成若干个小函数,导致函数有许多条件分支,从而降低执行性能.解决办法就是使用状态机.实现状态机的最简单方法就是使用switch.另外一个办法就是使用状态跳转表(goto).示例如下:{.switch(*cp){case'l':islong=1;++cp;break;case'h':isshort=1;++cp;break;default:}switch(*cp){case'd':.break;case'h':.break;default:}}使用状态跳转表{staticconstvoid*jumps1={['l']=&&do_l,['h']=&&do_h,['d']=&&do_d,['g']=&&do_g};staticconstvoid*jumps2={['d']=&&do_d,['g']=&&do_g};goto*jmps1[*cp];do_l:islong=1;++cp;gotojumps2[*cp];do_h:isshort=1;++cp;gotojumps2[*cp];do_d:.gotoout;do_g:.gotoout;out:}6.Profiling==对程序进行profile分两种:1)基于时间:找出最消耗时间的代码2)基于调用关系:找出函数调用次数和调用关系有些函数很小巧,可能被调用次数很多,但执行时间并不多.6.1grofprofilinggcc编译程序的时候加上-pg选项,gcc-cfoo.c-ofoo.o-pglink的时候加上-profile选项gcc-ofoofoo.o-profile6.2sprofprofiling不需要重新编译,只需设置环境LD_PROFILE=libc.so.6LD_PROFILE_OUTPUT=.然后运行程序,可以检查对库的调用.特别声明:1:资料来源于互联网,版权归属原作者2:资料内容属于网络意见,与本账号立场无关3:如有侵权,请告知,立即删除。
/
本文档为【使用gcc和glibc来优化程序 转载】,请使用软件OFFICE或WPS软件打开。作品中的文字与图均可以修改和编辑, 图片更改请在作品中右键图片并更换,文字修改请直接点击文字进行修改,也可以新增和删除文档中的内容。
[版权声明] 本站所有资料为用户分享产生,若发现您的权利被侵害,请联系客服邮件isharekefu@iask.cn,我们尽快处理。 本作品所展示的图片、画像、字体、音乐的版权可能需版权方额外授权,请谨慎使用。 网站提供的党政主题相关内容(国旗、国徽、党徽..)目的在于配合国家政策宣传,仅限个人学习分享使用,禁止用于任何广告和商用目的。

历史搜索

    清空历史搜索