发布于2021-03-14 05:56 阅读(902) 评论(0) 点赞(11) 收藏(4)
在现代的开发工作中,相信绝大部分的同学手头的项目都不是从第零行代码开始搭建的。各个语言都有自己流行的代码框架,如PHP的有Laravel、CodeIgniter、ThinkPHP等等。大家都是在自己的框架的基础上添加自己的业务代码逻辑,开启开发工作。还记得我们团队有位开发同学当时问过我一个问题,我们用xx框架这么重,一个用户请求过来即使什么也不干,都已经进行了那么多次的函数调用了,适合用来做接口开发吗?
我当时给她的回答是,没问题放心吧,函数调用的开销很小的,不必担心。但回答完她的问题之后,我回头一想,我只知道函数调用的开销很小,但是具体是多大,我心里并吃不准,这就在我心里又种下了草。后来终于抽空进行了一次实践研究,把草拔掉了。
测试代码很简单,这就是一个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左右。
我们用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个!。
如果有同学和我一样好奇结论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指令,每个指令大概含义如下:
mov $0x2,%edi
是为了调用函数做准备,把参数放到寄存器中。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指令就都水落石出了。
push %rbp
bp寄存器的值压入调用栈,即将main函数栈帧的栈底地址入栈(对应一次压栈操作,内存IO)mov %rsp,%rbp
被调函数的栈帧栈底地址放入bp寄存器,建立func函数的栈帧(一次寄存器操作)。mov %edi,-0x4(%rbp)
是从寄存器的地址-4的内存中取出,即获取输入参数(内存IO)mov $0x1,%eax
对应return 0
,即是将返回参数写到寄存器中(内存读IO)再接下来的两个执行令是进行调用栈的退栈,以便于返回到main函数继续执行。是指令3和指令4的逆操作。
leave q
等价于mov %rbp, %rsp
,寄存器操作retq
等价于pop %rbp
(内存IO)总结:8次CPU指令中大部分都是寄存器的操作,即使有“内存IO”,也是在栈上进行。而栈操作密集,符合局部性原理,早就被L1缓存住了,其实都是L1的IO,所以耗时很低。前面实验结果表明1次函数调用的开销是0.4ns, 耗时竟然小于1次真正物理内存IO的耗时(40ns左右),
不知道大家有没有人注意到,前面两次perf stat的结果中分别有如下两个提示
很多同学又会问题,你用的是C语言进行测试,性能当然高了。
好了,不抬杠,我们继续试一试不就完了么。就用php来继续实验一把。
<?php
function func(){
return true;
}
for($i=0;$i<10000000;$i++){
func();
}
实验结果:
php的函数调用确实比c的要慢很多,从不到1ns升高到了50ns左右。因为php又用c虚拟了一层指令集,这层指令集还需要变成CPU的指令集后才可以真正运行。但是要知道的是ns这个时间单位太小了,假如你用的框架特别变态,一个用户请求来了直接就搞了1000次的函数调用,那么消耗在函数调用上的时间会是50ns*1000=50us。这和代码框架化后给团队项目带来的便利性来对比的话,这点时间开销,我觉得仍然是可以忽略的。
开发内功修炼之CPU篇专辑:
我的公众号是「开发内功修炼」,在这里我不是单纯介绍技术理论,也不只介绍实践经验。而是把理论与实践结合起来,用实践加深对理论的理解、用理论提高你的技术实践能力。欢迎你来关注我的公众号,也请分享给你的好友~~~
原文链接:https://www.cnblogs.com/kfngxl/p/13910842.html
作者:gogogo
链接:http://www.phpheidong.com/blog/article/3038/3728e71740431bde62f0/
来源:php黑洞网
任何形式的转载都请注明出处,如有侵权 一经发现 必将追究其法律责任
昵称:
评论内容:(最多支持255个字符)
---无人问津也好,技不如人也罢,你都要试着安静下来,去做自己该做的事,而不是让内心的烦躁、焦虑,坏掉你本来就不多的热情和定力
Copyright © 2018-2021 php黑洞网 All Rights Reserved 版权所有,并保留所有权利。 京ICP备18063182号-4
投诉与举报,广告合作请联系vgs_info@163.com或QQ3083709327
免责声明:网站文章均由用户上传,仅供读者学习交流使用,禁止用做商业用途。若文章涉及色情,反动,侵权等违法信息,请向我们举报,一经核实我们会立即删除!