函数开销混淆在现代开发工作中,相信大部分同学的项目都不是从零行代码开始构建的。每种语言都有自己流行的代码框架,比如PHP的Laravel、CodeIgniter、ThinkPHP等。大家在自己的框架基础上添加自己的业务代码逻辑,开始开发工作。还记得当时我们组一个开发同学问过我一个问题。我们大量使用了xx框架。就算一个用户请求过来什么都不做,他已经调用了那么多函数,适合做界面开发。?我当时给她的回答是,没问题,不用担心,函数调用的开销很小,不用担心。但是回答完她的问题,回头一想,我只知道函数调用的开销很小,不知道有多大,这又在心里种草了。后来终于抽空进行了实践学习,拔草了。C语言测试测试代码很简单,就是一个for循环的函数调用。代码如下:#includeintfunc(intp){return1;}intmain(){inti;for(i=0;i<100000000;i++){func(2);}返回0;}函数调用耗时测试我们使用time命令进行耗时测试#gccmain.c-omain#time./mainreal0m0.335suser0m0.334ssys0m0.000s#perfstat./main......1,100,989,673instructions#1.37insnspercycle......但是上面的实验还有一个额外的开销,那就是for循环。我们单独算一下这个for的开销,把调用func()的那行注释掉,单独保留1亿个for循环,然后重新编译执行一遍。结果是time./mainreal0m0.293suser0m0.292ssys0m0.000sperfstat./main......301,252,997instructions#0.43insnspercycle......通过以上两步测试数据,(0.335-0.293)/100000000=0.4ns。我们可以得出结论1:每次c函数调用耗时0.4ns左右。函数调用的CPU指令数分析我们可以使用perf命令来统计程序运行的底层CPU指令数。一亿次函数调用统计结果如下:#perfstat./main......1,100,989,673instructions#1.37insnspercycle......去除for循环后,单次1亿次for循环统计如下:#perfstat./main......301,252,997instructions#0.43insnspercycle...通过这两个数据,(1,100,989,673-301,252,997)/100000000=8。于是我们得出结论2:每个c函数需要的CPU指令数为8条!.函数调用CPU指令分析如果有同学和我一样好奇结论2中每个c函数的CPU指令是做什么的,请跟我来,否则请开启3倍快进。还是上面的实验代码,我们通过gdb的反汇编和编译来查看其内部汇编执行过程。gcc-gmain.c-omain然后用gdb命令调试:gdb./mainstartdisassemblemov$0x2,%edi看到函数已经到了main函数,打印出main函数的汇编代码...=>0x0000000000400486<+4>:mov$0x2,%edi0x000000000040048b<+9>:callq0x400474……这是两条进入函数调用的CPU指令。每条指令的含义如下:指令1:mov$0x2,%edi准备调用函数,将参数放入寄存器。指令2:callq表示cpu开始执行func函数的代码段。接下来我们进入func函数内部看看:breakfuncrun此时函数停在func函数入口处,继续使用gdb的disassemble命令查看汇编指令:(gdb)disassembleDumpof函数func的汇编代码:0x0000000000400474<+0>:push%rbp0x0000000000400475<+1>:mov%rsp,%rbp0x000000000400478<+4>:mov%edi,-0x4(%rbp)=>0x0000000470004:mov$0x1,%eax400000000+12>:leaveq0x0000000000400481<+13>:retq汇编程序转储结束。这6条指令分别对应函数内部执行的操作和函数返回的操作。添加前两个,将揭示结论2中每个功能的8条CPU指令。指令3:push%rbp将bp寄存器的值压入调用栈,即主函数栈帧的栈底地址压入栈中(对应一次push操作,内存IO)指令4:mov%rsp,%rbp调用函数的栈帧栈底地址放入bp寄存器,func函数的栈帧建立(一次寄存器操作)。指令5:mov%edi,-0x4(%rbp)从寄存器地址-4的内存中取,即获取输入参数(内存IO)。指令6:mov$0x1,%eax对应return0,即会将return的参数写入寄存器(内存读取IO),接下来的两条执行命令是对调用栈进行unstack,从而返回到main函数继续执行。是指令3和指令4的逆运算指令7:leaveq相当于mov%rbp,%rsp,寄存器操作指令8:retq相当于pop%rbp(内存IO)总结:大部分8CPU指令是寄存器操作,即使有“内存IO”,也在栈上。栈操作是密集型的,符合局部性原则。早就被L1缓存了。其实都是L1的IO,所以耗时很低。前面的实验结果表明,一次函数调用的开销是0.4ns,实际上比一次真正的物理内存IO的耗时(40ns左右)还要少。不知道有没有人注意到指令是并行的。前面两个perfstats的结果有两个提示如下:0.43insnspercycle1.37insnspercycle这意味着现代CPU可以通过流水线并行处理CPU指令。当指令符合并行规则时,每个CPU周期内执行的指令数可能大于1。这就是CPU指令并行性的功劳。因此,加入函数调用后的时间消耗并没有增加太多。除了函数调用本身并不昂贵的原因外,还有一个原因是函数调用使CPU的流水线并行技术得以发挥,CPU每秒处理的指令数更多。.很多同学在PHP语言测试中都会出现问题。如果用C语言进行测试,性能当然高。“我正在使用PHP,这是一种脚本语言”“我正在使用Java,中间有一个虚拟机”“我正在使用......”好吧,让我们继续努力,我们会完成的.就用php继续实验吧。