美文网首页开发者内功修炼
函数调用的太多了会不会有性能问题?

函数调用的太多了会不会有性能问题?

作者: yanfeizhang | 来源:发表于2019-08-04 16:47 被阅读0次

在现代的开发工作中,相信绝大部分的同学手头的项目都不是从第零行代码开始搭建的。各个语言都有自己流行的代码框架,如PHP的有Laravel、CodeIgniter、ThinkPHP等等。大家都是在自己的框架的基础上添加自己的业务代码逻辑,开启开发工作。还记得我们团队有位开发同学当时问过我一个问题,我们用xx框架这么重,一个用户请求过来即使什么也不干,都已经进行了那么多次的函数调用了,适合用来做接口开发吗?
我当时给她的回答是,没问题放心吧,函数调用的开销很小的,不必担心。但回答完她的问题之后,我回头一想,我只知道函数调用的开销很小,但是具体是多大,我心里并吃不准,这就在我心里又种下了草。后来终于抽空进行了一次实践研究,把草拔掉了。

C语言测试

代码参见test01

# gcc main.c -o main  
# time ./main  
real    0m0.335s  
user    0m0.334s  
sys     0m0.000s  

#perf stat ./main  
......  
1,100,989,673 instructions              #    1.37  insns per cycle  
......  

为了去掉for循环对实验结果的影响,再把func()调用那行注释掉,再重新编译执行一遍,结果是

time ./main  
real    0m0.293s  
user    0m0.292s  
sys     0m0.000s  

perf stat ./main  
......  
301,252,997 instructions   #    0.43  insns per cycle
......  

可以计算得出每个c的函数调用耗时大约是(0.335-0.293)/100000000=0.4ns左右。需要的CPU指令数是(1,100,989,673-301,252,997)/100000000=8。

函数调用过程剖析

还是上述的实验代码,我们通过gdb的disassemble来查看一下其内部汇编执行过程
编译之,gcc -g main.c -o function1
再用gdb命令调试

gdb ./function1
start
disassemble
mov    $0x2,%edi

看到函数到了main函数处,并打印出了main函数的汇编代码

......
=> 0x0000000000400486 <+4>: mov    $0x2,%edi
   0x000000000040048b <+9>: callq  0x400474 <func>
......

其中mov $0x2,%edi是为了调用函数做准备,把参数放到寄存器中。
callq表示cpu开始执行func函数的代码段

break func
run

这时函数停在了func函数的入口处, 继续使用gdb的disassemble命令查看汇编指令

(gdb) disassemble
Dump of assembler code for function func:
   0x0000000000400474 <+0>: push   %rbp
   0x0000000000400475 <+1>: mov    %rsp,%rbp
   0x0000000000400478 <+4>: mov    %edi,-0x4(%rbp)
=> 0x000000000040047b <+7>: mov    $0x1,%eax
   0x0000000000400480 <+12>:    leaveq 
   0x0000000000400481 <+13>:    retq   
End of assembler dump.

其中push对应一次压栈操作(内存IO),
mov %rsp,%rbp对应一次寄存器操作。
mov %edi,-0x4(%rbp)是从寄存器的地址-4的内存中取出参数(内存IO)
mov $0x1,%eax对应return 0,即是将返回参数写到寄存器中(内存读IO)
leave q等价于mov %rbp, %rsp,寄存器操作
retq 等价于pop %rbp(内存IO)

总结:
一次函数调用大概对应4次内存IO,3次寄存器操作。前面实验结果表明1次函数调用的开销是0.4ns, 耗时竟然小于1次内存IO的耗时(1ns左右),原因是因为栈由于访问密集,早已经被CPU的高速缓存cache住了。所以耗时很低。

CPU指令并行: 不知道大家有没有人注意到,前面两次perf stat的结果中分别有如下两个提示

  • 0.43 insns per cycle
  • 1.37 insns per cycle
    这是说现代的CPU可以通过流水线的方式对CPU指令进行并行处理,当指令符合并行规则的时候,每个CPU周期内执行的指令数可能会大于1。所以增加函数调用后耗时并没有增加太多,除了函数调用本身开销不大的原因以外,还有一个原因就是函数调用让CPU的流水线并行技术得以施展,每秒处理的CPU指令数更多了。

PHP语言测试

很多同学又会问题,你用的是C语言进行测试,性能当然高了。

  • “我用的可是PHP,这可是脚本语言”
  • “我用的可是Java,中间可还有一层虚拟机”
  • “我用的可是...”

好了,不抬杠,我们继续试一试不就完了么。就用php来继续实验一把。

<?php  
function func(){  
    return true;  
}  
for($i=0;$i<10000000;$i++){  
    func();  
}  

实验结果:

  • php7: 1000W次耗时0.667s,减去0.140s的for循环耗时,平均每次函数调用耗时52ns
  • php53:1000W次耗时2.1s,减去0.5s的for循环耗时,平均每次耗时160ns

结论

php的函数调用确实比c的要慢很多,从不到1ns升高到了50ns左右。因为php又用c虚拟了一层指令集,这层指令集还需要变成CPU的指令集后才可以真正运行。但是要知道的是ns这个时间单位太小了,假如你用的框架特别变态,一个用户请求来了直接就搞了1000次的函数调用,那么消耗在函数调用上的时间会是50ns*1000=50us。这和代码框架化后给团队项目带来的便利性来对比的话,这点时间开销,我觉得仍然是可以忽略的。

个人公众号“开发内功修炼”,打通理论与实践的任督二脉。

相关文章

  • 函数调用的太多了会不会有性能问题?

    在现代的开发工作中,相信绝大部分的同学手头的项目都不是从第零行代码开始搭建的。各个语言都有自己流行的代码框架,如P...

  • js 防抖动

    问题:针对频繁触发scoll resize绑定的事件函数,有可能短时间多次触发时事件,影响性能。思路:多个函数调用...

  • JavaScript - 函数节流throttle

    函数节流: 函数被频繁调用,会造成性能问题。函数节流保证固定时间间隔内,无论被重复调用多少次都只执行一次。 应用场...

  • 函数节流和分时函数

    1.函数节流 函数的触发不是由用户来控制的,当函数被非常频繁调用时,会造成大的性能问题 window.onsize...

  • 内联函数

    解决函数调用效率的问题:函数之间调用,是内存地址之间的调用,当函数调用完毕之后还会返回原来函数执行的地址。函数调用...

  • 数据结构(五)--递归

    数据结构(五)--递归 首先思考一个问题,当A函数调用B函数,A函数是如何调用B函数的? 在一个函数调用期间,调用...

  • 5-函数调用方式 严格模式 作用域 变量/函数提升

    函数的调用方式 普通的函数调用方式直接调用(this丢失问题): window(非严格) | undefined...

  • 【Solidity学习笔记】外部函数的调用

    3.8 外部函数的调用 在Solidity中,有两种函数调用:内部函数调用和外部函数调用。内部函数调用是指一个函数...

  • JS函数调用

    js 里函数调用有4种模式:方法调用、正常函数调用、构造器函数调用、apply/call 调用。无论哪种函数调用除...

  • 解决函数的高频次调用的问题,防抖/节流函数

    问题 常见的问题就是函数的高频次调用,会极度消耗性能,严重的会把浏览器拖垮,卡死,最常见的是浏览器的窗口大小变化,...

网友评论

    本文标题:函数调用的太多了会不会有性能问题?

    本文链接:https://www.haomeiwen.com/subject/rgeydctx.html