使用gcc和glibc来优化程序转载.docx
- 文档编号:24850496
- 上传时间:2023-06-02
- 格式:DOCX
- 页数:25
- 大小:25.16KB
使用gcc和glibc来优化程序转载.docx
《使用gcc和glibc来优化程序转载.docx》由会员分享,可在线阅读,更多相关《使用gcc和glibc来优化程序转载.docx(25页珍藏版)》请在冰豆网上搜索。
使用gcc和glibc来优化程序转载
使用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/dandassigntoa
6).
如果有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
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 使用gcc和glibc来优化程序 转载 使用 gcc glibc 优化 程序