函数开销困惑

在古代的开发工作中,置信绝大部分的同学手头的我的项目都不是从第零行代码开始搭建的。各个语言都有本人风行的代码框架,如PHP的有Laravel、CodeIgniter、ThinkPHP等等。大家都是在本人的框架的根底上增加本人的业务代码逻辑,开启开发工作。还记得咱们团队有位开发同学过后问过我一个问题,咱们用xx框架这么重,一个用户申请过去即便什么也不干,都曾经进行了那么屡次的函数调用了,适宜用来做接口开发吗?
我过后给她的答复是,没问题释怀吧,函数调用的开销很小的,不用放心。但答复完她的问题之后,我回头一想,我只晓得函数调用的开销很小,然而具体是多大,我心里并吃不准,这就在我心里又种下了草。起初终于抽空进行了一次实际钻研,把草拔掉了。

C语言测试

测试代码很简略,这就是一个for循环的函数调用。代码如下:

#include <stdio.h>  int func(int p){       return 1;}  int main()  {      int i;      for(i=0; i<100000000; i++){          func(2);      }      return 0;  }

函数调用耗时测试

咱们用time命令来进行耗时测试

# 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循环。咱们独自计算一下这个for的开销,把func()调用那行正文掉,独自保留1亿次的for循环,再从新编译执行一遍。后果是

time ./main  real    0m0.293s  user    0m0.292s  sys     0m0.000s  perf stat ./main  ......  301,252,997 instructions   #    0.43  insns per cycle......  

通过下面两步测试的数据,(0.335-0.293)/100000000=0.4ns。咱们能够得出论断1:每个c函数调用耗时大概是0.4ns左右。

函数调用CPU指令数剖析

咱们用perf命令能够统计到程序运行的底层CPU指令个数。1亿次的函数调用统计后果如下:

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

去掉for循环后,独自1亿次的for循环统计如下:

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

通过这两个数据,(1,100,989,673-301,252,997)/100000000=8个。所以咱们得出论断2:每个c函数须要的CPU指令数是8个!

函数调用CPU指令分析

如果有同学和我一样好奇论断2中的每个c函数的CPU指令到底干了些啥,请和我一起来,否则请开启3倍速快进。还是上述的试验代码,咱们通过gdb的disassemble来查看一下其外部汇编执行过程,编译之。

gcc -g main.c -o main

再用gdb命令调试:

gdb ./mainstartdisassemblemov    $0x2,%edi

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

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

这是进入函数调用的两个CPU指令,每个指令大略含意如下:

  • 指令1:mov $0x2,%edi是为了调用函数做筹备,把参数放到寄存器中。
  • 指令2:callq示意cpu开始执行func函数的代码段。

接下来让咱们进入到func函数外部看一下:

break funcrun

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

(gdb) disassembleDump 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.

这6个指令是对应在函数外部执行,以及函数返回的操作。加上后面2个,这样在论断2中的每个函数8个CPU指令就都上不着天;下不着地了。

  • 指令3:push %rbp bp寄存器的值压入调用栈,行将main函数栈帧的栈底地址入栈(对应一次压栈操作,内存IO)
  • 指令4:mov %rsp,%rbp被调函数的栈帧栈底地址放入bp寄存器,建设func函数的栈帧(一次寄存器操作)。
  • 指令5:mov %edi,-0x4(%rbp)是从寄存器的地址-4的内存中取出,即获取输出参数(内存IO)
  • 指令6:mov $0x1,%eax对应return 0,即是将返回参数写到寄存器中(内存读IO)

再接下来的两个执行令是进行调用栈的退栈,以便于返回到main函数继续执行。是指令3和指令4的逆操作。

  • 指令7:leave q等价于mov %rbp, %rsp,寄存器操作
  • 指令8:retq 等价于pop %rbp(内存IO)

总结:8次CPU指令中大部分都是寄存器的操作,即便有“内存IO”,也是在栈上进行。而栈操作密集,合乎局部性原理,早就被L1缓存住了,其实都是L1的IO,所以耗时很低。后面试验结果表明1次函数调用的开销是0.4ns, 耗时居然小于1次真正物理内存IO的耗时(40ns左右),

指令并行

不晓得大家有没有人留神到,后面两次perf stat的后果中别离有如下两个提醒

  • 0.43 insns per cycle
  • 1.37 insns per cycle

这是说古代的CPU能够通过流水线的形式对CPU指令进行并行处理,当指令合乎并行规定的时候,每个CPU周期内执行的指令数可能会大于1。这就是CPU指令并行的功绩。 所以减少函数调用后耗时并没有减少太多,除了函数调用自身开销不大的起因以外,还有一个起因就是函数调用让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。这和代码框架化后给团队我的项目带来的便利性来比照的话,这点工夫开销,我感觉依然是能够疏忽的。



开发内功修炼之CPU篇专辑:

  • 1.你认为你的多核CPU都是真核吗?多核“假象”
  • 2.据说你只知内存,而不知缓存?CPU示意很伤心!
  • 3.TLB缓存是个神马鬼,如何查看TLB miss?
  • 4.过程/线程切换到底须要多少开销?
  • 5.协程到底比线程牛在什么中央?
  • 6.软中断会吃掉你多少CPU?
  • 7.一次零碎调用开销到底有多大?
  • 8.一次简略的php申请redis会有哪些开销?
  • 9.函数调用太多了会有性能问题吗?

我的公众号是「开发内功修炼」,在这里我不是单纯介绍技术实践,也不只介绍实践经验。而是把实践与实际联合起来,用实际加深对实践的了解、用实践进步你的技术实际能力。欢送你来关注我的公众号,也请分享给你的好友~~~