分享

C/C++之深入分析inline函数

 心不留意外尘 2016-04-15

from http://blog.csdn.net/prsniper/article/details/7297315

2012.02

C/C++以其飚捍的执行效率和近乎ASM的强大功能,使得这类语言在IT技术发展的惊涛骇浪中,久战不衰.

C/C++中一个重要的特色就是内联函数(在函数的声明代码有inline),那么我们就深入探讨一下这个神秘的小家伙吧.

 

如果跨过高级语言进入汇编层面,函数的调用是通过堆栈完成的,往往函数参数越多,堆栈操作也越多,完成后堆栈的清理任务也更繁重.而内联函数则是面向编译的小伎俩,直接将内联函数的源码,复制到主调函数中,省去了堆栈操作,一定程度上提高了程序的性能(CPU缓存正在改变这一点,我们后面再说),先来看一段简单的代码:

  1. // http://blog.csdn.net/prsniper  
  2.   
  3. #include "stdafx.h"  
  4.   
  5. inline void fnHelloWorld3();  
  6.   
  7. int main(int argc, char* argv[])    /*命令行argc:参数数量,argv[]参数数组,字符串形式*/  
  8. {   void fnHelloWorld2();   //声明函数  
  9.   
  10.     printf("Hello World1!\n");  
  11.     fnHelloWorld2();    //call function  
  12.     fnHelloWorld3();    //call inline function  
  13.     return 0;  
  14. }  
  15.   
  16. void fnHelloWorld2()  
  17. {   printf("Hello World2!\n");  
  18. }  
  19.   
  20. inline void fnHelloWorld3()  
  21. {   printf("Hello World3!\n");  
  22. }  


又是一个hello world程序.在VC6中我们直接进入DASM进行DEBUG(VC6以上版本也可以的,比如VC.NET2003)

7:    int main(int argc, char* argv[])    /*命令行argc:参数数量,argv[]参数数组,字符串形式*/
8:    {   void fnHelloWorld2();   //声明函数
00401030   push        ebp    ;基址指针入栈
00401031   mov         ebp,esp    ;堆栈栈顶指针传送到基枝指针,其实就是不管前面的堆栈,一切从这里开始
00401033   sub         esp,40h    ;ESP减少0x40=64字节,这就是函数的堆栈空间
00401036   push        ebx    ;存储器指针入栈
00401037   push        esi    ;源指针
00401038   push        edi    ;目的指针
00401039   lea         edi,[ebp-40h]    ;不改变EBP的情况下,将EBP-0x40的值传给EDI,见下
0040103C   mov         ecx,10h    ;串操作计数,要填充那么多次
00401041   mov         eax,0CCCCCCCCh
00401046   rep stos    dword ptr [edi]    ;填充堆栈空间为0xCCCCCC,这就是为什么VC的局部变量默认是0xCC...
9:
10:       printf("Hello World1!\n");    //常规调用
00401048   push        offset string "Hello World1!\n" (0042001c)    ;字符串指针入栈
0040104D   call        printf (00401130)    ;调用PRINTF函数
00401052   add         esp,4    ;栈顶指针加一个DWORD,其实就是删去这个字符串变量
11:       fnHelloWorld2();    //call function
00401055   call        @ILT+0(fnHelloWorld2) (00401005)    ;@ILT我还没完全弄清楚,不敢乱说,个人认为是便宜常量(见下)
12:       fnHelloWorld3();    //call inline function
0040105A   call        @ILT+10(fnHelloWorld3) (0040100f)
13:       return 0;
0040105F   xor         eax,eax    ;XOR自身其实就是将自身清0
14:   }
00401061   pop         edi    ;压入堆栈的寄存器,依次复原,又是一堆罗嗦的操作
00401062   pop         esi
00401063   pop         ebx
00401064   add         esp,40h
00401067   cmp         ebp,esp
00401069   call        __chkesp (004011b0)    ;堆栈检查,你可以继续跟踪,好长...
0040106E   mov         esp,ebp
00401070   pop         ebp
00401071   ret   ;终于函数调用结束了

....

@ILT+0(?fnHelloWorld2@@YAXXZ):
00401005   jmp         fnHelloWorld2 (00401090)    ;跳转到这里,相当于直接执行PRINTF了,中间只经历了2个JMP无条件跳转
@ILT+5(_main):
0040100A   jmp         main (00401030)
@ILT+10(?fnHelloWorld3@@YAXXZ):
0040100F   jmp         fnHelloWorld3 (004010e0)

 

现在,我们可以看到,内联的函数其实就是很卑鄙地给函数执行部分加一个标签,调用的时候直接跳转到这里执行.

省去了一大堆堆栈操作,然而是否可以任意使用inline呢?答案是否定的,再看一段代码(原代码捎加修改而已):

  1. // http://blog.csdn.net/prsniper  
  2.   
  3. #include "stdafx.h"  
  4.   
  5. inline void fnHelloWorld3();  
  6.   
  7. int main(int argc, char* argv[])    /*命令行argc:参数数量,argv[]参数数组,字符串形式*/  
  8. {   unsigned char byt[1024];  
  9.     void fnHelloWorld2(unsigned char *b);   //声明函数  
  10.   
  11.     printf("Hello World1!\n");  
  12.     fnHelloWorld2(byt); //call function  
  13.     fnHelloWorld3();    //call inline function  
  14.     return 0;  
  15. }  
  16.   
  17. void fnHelloWorld2(unsigned char *b/*b[128]*/)  
  18. {   int i;  
  19.     unsigned char bv[128];  
  20.     printf("Hello World:");  
  21.     for(i = 0; i < 128; i++)  
  22.     {   printf("%d,", b[i]);  
  23.         bv[i] = b[i];  
  24.     }  
  25.     printf("\b!\n");  
  26. }  
  27.   
  28. inline void fnHelloWorld3()  
  29. {   printf("Hello World3!\n");  
  30. }  


我们在C/C++层面DEBUG,发现循环中bv[i] = b[i];没有生效,实际执行中也是,那我们就看看它更深层的机理吧:


17:   void fnHelloWorld2(unsigned char *b/*b[128]*/)
18:   {   int i;
0040B820   push        ebp    ;相同的地方就不赘述了
0040B821   mov         ebp,esp
0040B823   sub         esp,0C4h   ;这里用更大的堆栈空间0xC4=196,是为了能容下局部变量
0040B829   push        ebx
0040B82A   push        esi
0040B82B   push        edi
0040B82C   lea         edi,[ebp-0C4h]
0040B832   mov         ecx,31h
0040B837   mov         eax,0CCCCCCCCh
0040B83C   rep stos    dword ptr [edi]
19:       unsigned char bv[128];
20:       printf("Hello World:");
0040B83E   push        offset string "Hello World:" (00420034)
0040B843   call        printf (00401130)    ;先执行一次PRINTF
0040B848   add         esp,4

21:       for(i = 0; i < 128; i++)    //下面就是循环编译后的汇编代码啦
0040B84B   mov         dword ptr [ebp-4],0    ;基址指针-4就是变量i,初始化为0
0040B852   jmp         fnHelloWorld2+3Dh (0040b85d)    ;转到CMP
0040B854   mov         eax,dword ptr [ebp-4]    ;经过循环来到这里,将i的值传送到EAX累加器,自加(i++)
0040B857   add         eax,1
0040B85A   mov         dword ptr [ebp-4],eax    ;结果回到i的地址,完成i++
0040B85D   cmp         dword ptr [ebp-4],80h    ;i与0x80=128比较
0040B864   jge         fnHelloWorld2+72h (0040b892)    ;如果i大于或者等于128则跳转到循环以后执行源码第25行
22:       {   printf("%d,", b[i]);    //打印没有问题,就不解释了,偷懒就是艺术
0040B866   mov         ecx,dword ptr [ebp+8]
0040B869   add         ecx,dword ptr [ebp-4]
0040B86C   xor         edx,edx
0040B86E   mov         dl,byte ptr [ecx]
0040B870   push        edx
0040B871   push        offset string "%d," (00420044)
0040B876   call        printf (00401130)
0040B87B   add         esp,8
23:           bv[i] = b[i];
0040B87E   mov         eax,dword ptr [ebp+8]   ;这里解释一下EBP[=调用前ESP]是堆栈,-4是第一个局部变量,+8则是第一个参数
0040B881   add         eax,dword ptr [ebp-4]    ;第一个参数的地址+变量i的偏移得到b[i]
0040B884   mov         ecx,dword ptr [ebp-4]    ;一样计算偏移,存储在ECX中,下面将作为局部变量BV的索引
0040B887   mov         dl,byte ptr [eax]    ;EDX的低8位存储参数1对应偏移的值 b[i]
0040B889   mov         byte ptr [ebp+ecx-84h],dl    ;EBP-i-0x80-4这样好理解多(我看了下,如果字节倒序存储是没有问题的,但是内存地址的值没有被更改)
24:       }
0040B890   jmp         fnHelloWorld2+34h (0040b854)    ;跳转回到累加操作,准备下一个循环
25:       printf("\b!\n", i);
0040B892   mov         eax,dword ptr [ebp-4]
0040B895   push        eax
0040B896   push        offset string "\x08!\n" (00420030)
0040B89B   call        printf (00401130)
0040B8A0   add         esp,8

26:   }
0040B8A3   pop         edi
0040B8A4   pop         esi
0040B8A5   pop         ebx
0040B8A6   add         esp,0C4h
0040B8AC   cmp         ebp,esp
0040B8AE   call        __chkesp (004011b0)
0040B8B3   mov         esp,ebp
0040B8B5   pop         ebp
0040B8B6   ret

代码不能乱用,就像女人不能乱碰,否则在特定的情况下,让你痛不欲生!

网上有资料说:不能包含循环、switch、if语句,我没有完整实验,不过循环是不行了,至于你信不信,反正我信了.

下面回到开头,我们说的CPU缓存在改变这一点,做一个简单的叙述:

直接的说,现在一般CPU的缓存比较大,如我的烂U:AMD双核 6000+ 分别有64K的L1(Level1)数据和代码缓存,好一点的可以到256K,我的L2有2M的数据代码缓存.

通常来讲,L1的存取速度直接就是寄存器的速度,理论可以达到CPU时钟频率,可以看作是不需要时间的,而内存一般是每秒10G左右的读写速度.

CPU将一些代码从内存缓存,再需要的时候不再访问内存,如果内联函数过多,将导致缓存承受不了那么多的空间,而反复缓存,就像WEB开发中的缓存,重复缓存还不如直接从磁盘读取..

 

inline函数有它强悍的地方,用于高效程序,加密等可以说是"牛X",然而鉴于上面的困境,有没有什么好的方法呢?

方法当然是有:

1.自己把内联函数的过程再写一遍(累,跟主调函数融合难)

2.改为非内联函数(吐了)

3.自己写汇编代码(这个是我想到的比较好的方法了 - -)

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多