使用gcc和glibc来优化程序转载共20页文档Word文档格式.docx
《使用gcc和glibc来优化程序转载共20页文档Word文档格式.docx》由会员分享,可在线阅读,更多相关《使用gcc和glibc来优化程序转载共20页文档Word文档格式.docx(23页珍藏版)》请在冰点文库上搜索。
![使用gcc和glibc来优化程序转载共20页文档Word文档格式.docx](https://file1.bingdoc.com/fileroot1/2023-5/1/0acbb4e8-f365-4960-90d1-07b22b3c9541/0acbb4e8-f365-4960-90d1-07b22b3c95411.gif)
if(sizeof(int)==sizeof(longint)||(type==0))
sizeof运算总是在编译时进行,因此增加的条件表达式总是在编译时计算.
如果longint和int确实相同,那么这个函数就可以被编译器优化.
进一步优化,利用limits.h中定义的宏
#includelimits.hlongintadd(longinta,void*ptr,inttype)
#ifLONG_MAX!
=INT_MAXif(type==0)
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
正确的写法应该是:
do{\
}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)\
__externsion____typeof__((a)+(b)+(c))c__=(c);
这里,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)?
sin(val):
csin(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,这并不是我们
期望的.
正确的实现方法是:
({__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))\
__tgmres=sin(val);
__tgmres=csin(val);
__tgmres=sinf(val);
__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).对应
的源代码为
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);
优化后的结果为:
serv=htons(serv);
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)))
{returna+b;
}
经过编译优化后,生成的代码时
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内部函数
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
操作,因此更高效的作法是:
size_tslen;
size_tbuflen;
slen=strlen(s)+1;
buflen=strlen(buf);
if(buflen+slenbufmax)
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)