共计 3466 个字符,预计需要花费 9 分钟才能阅读完成。
函数开销困惑
在古代的开发工作中,置信绝大部分的同学手头的我的项目都不是从第零行代码开始搭建的。各个语言都有本人风行的代码框架,如 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 ./main
start
disassemble
mov $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 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.
这 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. 函数调用太多了会有性能问题吗?
我的公众号是「开发内功修炼」,在这里我不是单纯介绍技术实践,也不只介绍实践经验。而是把实践与实际联合起来,用实际加深对实践的了解、用实践进步你的技术实际能力。欢送你来关注我的公众号,也请分享给你的好友~~~