from http://blog.csdn.net/prsniper/article/details/7297315 2012.02 C/C++以其飚捍的执行效率和近乎ASM的强大功能,使得这类语言在IT技术发展的惊涛骇浪中,久战不衰.
C/C++中一个重要的特色就是内联函数(在函数的声明代码有inline),那么我们就深入探讨一下这个神秘的小家伙吧.
如果跨过高级语言进入汇编层面,函数的调用是通过堆栈完成的,往往函数参数越多,堆栈操作也越多,完成后堆栈的清理任务也更繁重.而内联函数则是面向编译的小伎俩,直接将内联函数的源码,复制到主调函数中,省去了堆栈操作,一定程度上提高了程序的性能(CPU缓存正在改变这一点,我们后面再说),先来看一段简单的代码:
- // http://blog.csdn.net/prsniper
-
- #include "stdafx.h"
-
- inline void fnHelloWorld3();
-
- int main(int argc, char* argv[]) /*命令行argc:参数数量,argv[]参数数组,字符串形式*/
- { void fnHelloWorld2(); //声明函数
-
- printf("Hello World1!\n");
- fnHelloWorld2(); //call function
- fnHelloWorld3(); //call inline function
- return 0;
- }
-
- void fnHelloWorld2()
- { printf("Hello World2!\n");
- }
-
- inline void fnHelloWorld3()
- { printf("Hello World3!\n");
- }
又是一个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呢?答案是否定的,再看一段代码(原代码捎加修改而已):
- // http://blog.csdn.net/prsniper
-
- #include "stdafx.h"
-
- inline void fnHelloWorld3();
-
- int main(int argc, char* argv[]) /*命令行argc:参数数量,argv[]参数数组,字符串形式*/
- { unsigned char byt[1024];
- void fnHelloWorld2(unsigned char *b); //声明函数
-
- printf("Hello World1!\n");
- fnHelloWorld2(byt); //call function
- fnHelloWorld3(); //call inline function
- return 0;
- }
-
- void fnHelloWorld2(unsigned char *b/*b[128]*/)
- { int i;
- unsigned char bv[128];
- printf("Hello World:");
- for(i = 0; i < 128; i++)
- { printf("%d,", b[i]);
- bv[i] = b[i];
- }
- printf("\b!\n");
- }
-
- inline void fnHelloWorld3()
- { printf("Hello World3!\n");
- }
我们在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.自己写汇编代码(这个是我想到的比较好的方法了 - -)
|