zoukankan      html  css  js  c++  java
  • 初学者疑惑:C语言中,函数反复调用会有什么问题?

    函数开销困惑

    在现代的开发工作中,相信绝大部分的同学手头的项目都不是从第零行代码开始搭建的。各个语言都有自己流行的代码框架,如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指令 ,每个指令大概含义如下:

    mov $0x2,%edi

    callq

    接下来让我们进入到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 %rbpbp寄存器的值压入调用栈,即将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。这和代码框架化后给团队项目带来的便利性来对比的话,这点时间开销,我觉得仍然是可以忽略的。


     

    好了,今天的分享就到这里了。看到这里,你是不是对“C语言”又有了一点新的认知呢~如果你喜欢这篇文章的话,动动小指,加个关注哦~


     

    最后,如果你也想成为程序员,想要快速掌握编程,这里为你分享一个学习企鹅圈子!

    里面有资深专业软件开发工程师,在线解答你的所有疑惑~C语言入门“so easy”

    资料包含:编程入门、游戏编程、课程设计、黑客等。

    编程学习书籍:


     

    编程学习视频:


     
  • 相关阅读:
    HTML/CSS
    Python字符编码
    软件测试遇到的问题积累
    数学
    经济学路谱
    工具
    DataStage
    Shell编程—定时任务
    WebLogic部署
    imageView-scaleType 图片压缩属性
  • 原文地址:https://www.cnblogs.com/mu-ge/p/13933444.html
Copyright © 2011-2022 走看看