Compiler- 尾调用

news2024/11/25 17:17:57

我们还是用例子来引入本次要探讨的问题--尾调用

#include <stdio.h>

int fib(int a)
{
    return a <= 2 ? 1 : fib(a - 1) + fib(a - 2);
}

int main()
{
    int n,result;
    scanf("%d",&n);

    result = fib(n);

    printf("result is %d.\n",result);
    return 0;
}

这段代码的含义应该很清晰。那下面这一段呢?

#include <stdio.h>

int repeat = 0;

int fib(int a)
{
    int result;
    if (a <= 2) return 1;
    if ((a - 1) <= 2) {   // a <= 3 的情况
        result = 1;
    } else {
        result = fib(a - 2) + fib(a - 3);
    }

    if ((a - 2) <= 2) {   // a <= 4 的情况
        result += 1;
    } else {
        result += fib(a - 3) + fib(a - 4);
    }
    return result;
}

int main()
{
    int n;
    int result;
    scanf("%d",&n);
    result = fib(n);

    printf("result is %d.\n",result);
    return 0;
}

这两个代码在功能上是一致的,只不过第二个代码把a <= 3 和 a <= 4 的情况也单独拎出来,类似于之前提到的循环展开(Compiler- 循环展开_青衫客36的博客-CSDN博客),也是为了提高效率

我们对上面的两个程序稍作修改,加上计时函数,如下所示

// fib.c
#include <stdio.h>
#include <time.h>

clock_t to_duration_in_ms(clock_t start, clock_t end) 
{
    return 1000 * (end - start) / CLOCKS_PER_SEC;
}

int repeat = 0;

int fib(int a)
{
    repeat ++;
    return a <= 2 ? 1 : fib(a - 1) + fib(a - 2);
}

int main()
{
    int n,result;
    clock_t start, end;
    scanf("%d",&n);

    start = clock();
    result = fib(n);
    end = clock();

    printf("result is %d,time is %ldms,repeat is %d.\n",result,to_duration_in_ms(start,end),repeat);
    return 0;
}
// fib1.c
#include <stdio.h>
#include <time.h>

clock_t to_duration_in_ms(clock_t start, clock_t end) 
{
    return 1000 * (end - start) / CLOCKS_PER_SEC;
}

int repeat = 0;

int fib(int a)
{
    int result;
    repeat ++;
    if (a <= 2) return 1;
    if ((a - 1) <= 2) {
        result = 1;
    } else {
        result = fib(a - 2) + fib(a - 3);
    }

    if ((a - 2) <= 2) {
        result += 1;
    } else {
        result += fib(a - 3) + fib(a - 4);
    }
    return result;
}

int main()
{
    int n,result;
    clock_t start, end;
    scanf("%d",&n);

    start = clock();
    result = fib(n);
    end = clock();

    printf("result is %d,time is %ldms,repeat is %d.\n",result,to_duration_in_ms(start,end),repeat);
    return 0;
}

然后来对比一下这两个程序的运行时间time和重复次数repeat

可以看到两个程序的运行结果是一样的,但是运行时间和重复次数明显第二个更优。

为什么会出现这样的差别呢?

我们以fib(3)为例,来看看在这两个代码中是如何执行的。

●  对于第一段代码,fib(3)->fib(2)+fib(1);fib(2)进行递归,返回1;fib(1)进行递归,返回1。

●  对于第二段代码,在两个if条件判断的第一个分支处返回。

所以,第二段代码节省了不少的函数调用的时间。而我们知道,函数调用,要分配栈,要传参总是要花费时间的。

下面我们来看看编译器优化的效果,使用GCC进行编译,选择-O3,查看运行时间。

相比于之前的386ms和328ms,经过O3优化后的132ms还是很厉害的hh~

那如果不用递归,而是用循环的方式计算斐波那契数列呢?我们来看下面代码的执行时间。

// fibc.c
#include <stdio.h>
#include <time.h>
clock_t to_duration_in_ms(clock_t start, clock_t end) 
{
    return 1000 * (end - start) / CLOCKS_PER_SEC;
}

int repeat = 0;

int fibc(int n)
{ 
    int f1 = 1;
    int f2 = 1;
    int result;
    if(n == 1 || n == 2) return 1;
    else {
        for(int i = 2; i < n; i++)
        {
            result = f1 + f2;
            f1 = f2;
            f2 = result;
        }
    }
    return result;
}

int main()
{
    int n;
    int result;
    clock_t start, end;
    scanf("%d",&n);
    start = clock();
    result = fibc(n);
    end = clock();
    printf("result is %d,time is %dms,repeat is %d.\n",result,to_duration_in_ms(start,end),repeat);
    return 0;
}

运行时间为惊人的0ms,由此可以体会到函数调用、递归执行的效率问题。

我们在学习递归和迭代的时候,也会知道递归的代码容易看懂,而迭代展开的代码不好懂。

那有没有一种方法既是递归,并且效率又比较高呢?(本质上是降低函数调用的代价)

这种方式是尾递归,特点是,这种调用在整个函数的最后一步,它调用完之后整个函数就结束了,由于这是函数最后一步,做完之后函数调用堆栈框架也可以拆除了。注意,此种写法,斐波那契数列的加法操作是在参数传递这个过程中完成的。

因为这个调用发生在函数的最后一部分,所以在优化时,每当进行函数调用,就没必要重新分配栈了,这样就把函数调用的代价降到很低。

#include <stdio.h>
#include <time.h>
clock_t to_duration_in_ms(clock_t start, clock_t end) 
{
    return 1000 * (end - start) / CLOCKS_PER_SEC;
}

int repeat = 0;

int fibtail(int n,int ret1, int ret2)
{
    if(n==1) return ret1;
    else return fibtail(n-1,ret2, ret1+ret2);
}

int main()
{
    int n;
    int result;
    clock_t start, end;
    scanf("%d",&n);
    start = clock();
    result = fibtail(n,1,1);
    end = clock();

    printf("result is %d,time is %dms,repeat is %d.\n",result,to_duration_in_ms(start,end),repeat);
}

我们看一下这个程序的运行时间

我们来分析一个简单的尾调用

#include <stdio.h>

int x = 1;
int g(int z)
{
    return x + z;
}

int f(int y)
{
    int x = y + 1;
    return g(y*x);
}

int main(){

    printf("The result is %d.\n",f(3));
    return 0;
}

看一下生成的汇编代码。

以下是开启-O3优化的情况: 

可以看到,在函数f和g中,代码得到了大幅的缩减。甚至,在优化之后,两个函数都没有分配栈空间,直接进行了计算。再进一步的,函数f并没有调用函数g,而是直接把函数g的工作给做了。所以,这种优化真是很了不起的。


以上为中科大软件学院《编译工程》课后总结,感谢郭燕老师的倾心教授,老师讲的太好啦(^_^) 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/459938.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JavaWeb02(Servlet页面跳转方式表单提交方式)

目录 一.servlet 1.1 什么是servlet? 1.2 实现接口,初始代码 1.3 学会配置和映射 1.4 掌握servlet的生命周期 生命周期的各个阶段 1.5 获取servlet初始化参数和上下文参数 1.5.1 初始代码 推荐使用 1.5.2 初始化参数 1.5.3 上下文参数 1.6 servlet应用:处理用户登…

多处理器的汇编编程

多处理器编程&#xff0c;本质上&#xff0c;就是把MR给每个处理器复制一份 每个处理器拿到MR&#xff0c;形成了自己的缓存内存空间&#xff0c;然后再在运行期间把运算结果写入共享内存 把i做成一条指令 使用asm嵌入汇编&#xff0c;向sum的寄存器直接写入1的值 把C语言转…

【Python入门第五十三天】Python丨NumPy 中的随机数

什么是随机数&#xff1f; 随机数并不意味着每次都有不同的数字。随机意味着无法在逻辑上预测的事物。 伪随机和真随机 计算机在程序上工作&#xff0c;程序是权威的指令集。因此&#xff0c;这意味着必须有某种算法来生成随机数。 如果存在生成随机数的程序&#xff0c;则…

10个必备的建筑可视化3dmax插件

当日复一日地处理项目时&#xff0c;很容易陷入舒适但效率不高的工作流程中。 插件是在不牺牲工作质量的情况下改进和加快工作流程的好方法。 尤其是在建筑可视化时&#xff0c;快节奏的行业往往需要艺术家灵活机智。 在本文中&#xff0c;我们将介绍 10 个最好的 3ds Max 插件…

Springboot 整合 JPA 及 Swagger2

首先是官方文档&#xff1a; Spring Data JPA - Reference Documentationhttps://docs.spring.io/spring-data/jpa/docs/2.2.4.RELEASE/reference/html/#repositories.query-methods 1、JPA相关概念 2、创建 Springboot 项目 修改 pom 文件&#xff0c;可以直接进行复制粘贴&a…

百度APP iOS端包体积50M优化实践(二) 图片优化

**一、前言删除线格式 ** 在上一篇文章&#xff0c;我们介绍了包体积优化的必要性、安装包组成部分和生成过程、国内外大厂APP包体积分析、百度APP包体积优化技术方案及各项收益&#xff0c;本文重点讲述图片优化&#xff0c;解压IPA包后发现&#xff0c;百度APP中asset和bund…

Seurat -- Normalize Data

brief seurat提供的教学里面包含了Standard pre-processing workflow,workflow包括QC&#xff0c;normalization&#xff0c;scale data &#xff0c;detection of highly variable features。其中 normalization就有蛮多方法的&#xff0c;seurat自己就提供了两种&#xff0c…

ChatGpt接入Word文档,让你秒变职场达人!

今天跟大家分享下我们如何使用VBA代码&#xff0c;将ChatGpt接入Word文档&#xff0c;操作非常的简单&#xff0c;但是开始之前我们需要做2项准备 1. 获取ChatGpt的API 2. 魔法上网 准备好这2件事后&#xff0c;我们就可以着手制作了: 一&#xff0c;设置代码 二&…

微软的“牛头怪时刻”

2014年&#xff0c;当萨提亚纳德拉接任微软CEO时&#xff0c;他面对的是一家停滞且难以在快速发展的技术领域保持竞争优势的公司。自那以后&#xff0c;纳德拉将其重点从传统操作系统和生产力软件&#xff0c;转向云计算和人工智能&#xff0c;被认为重振了微软。​ 让我们以O…

ThreadPoolExecutor源码阅读流程图

1.创建线程池 public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue) {this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,Executors.defaultThreadFactory(), def…

shell脚本编程规范与变量

目录 一.shell脚本的概述2.1 shell的作用 三. shell脚本的作用3.1 编写第一个shell脚本3.1.1 Shell 脚本的构成&#xff1a;3.1.2 脚本的执行方式 三. 重定向与管道符操作3.2 重定向操作3.2 管道操作符号 四. shell的变量的作用&#xff0c;类型4.1 定义变量4.2 命名的规则4.3 …

辛弃疾最有代表性的十首词

辛弃疾的词&#xff0c;风格多样&#xff0c;题材广阔&#xff0c;几乎涉及到生活中的各个方面&#xff0c;从爱国情怀到日常生活&#xff0c;甚至连戒酒这种事都能写入词中。辛弃疾也是两宋词人中&#xff0c;存词最多的作家之一&#xff0c;现存的六百多首作品。 辛弃疾的词…

【数据结构:线性表】单链表

在学习了顺序表&#xff0c;我们可能会对其有一些思考&#xff1a; 中间/头部的插入删除&#xff0c;时间复杂度为O(N)增容需要申请新空间&#xff0c;拷贝数据&#xff0c;释放旧空间。会有不小的消耗。增容一般是呈2倍的增长&#xff0c;势必会有一定的空间浪费。例如当前容…

第四次工业革命的里程碑-chatgpt

文章目录 一、 介绍二、 训练数据方法、数据来源三、 能帮你做什么做表格论文降重写文案、周报写代码改bug写注释写作业制作游戏策划方案 四、 搭建自己的chatgpt方法五、 安全、安全试用chatgpt的方法六、 几款类似chatgpt的工具七、 优点八、 缺点九、下一步的期待十、 总结 …

vue中vue-cli项目各种报错

目录 sockjs.js报错 [WDS] Disconnected报错 假如有以上报错&#xff0c;首先看下index.html有没有这句 <meta http-equiv"Content-Security-Policy" content"upgrade-insecure-requests"> 是限制资源获取&#xff1a;限制网页当中一系列的资源获…

OkHttp3源码解析 - 拦截器

系列文章目录 第一章 OkHttp3源码解析 - 请求流程 第二章 OkHttp3源码解析 - 拦截器 第三章 OkHttp3源码解析 - 连接机制和缓存机制 文章目录 系列文章目录前言一、五大内置拦截器二、拦截器分发流程1.RetryAndFollowUpInterceptor-重试重定向拦截器2.BridgeInterceptor-桥接拦…

用友BIP助力中国领先企业数智化国产替代

随着数字经济的快速发展&#xff0c;软件的重要性日益凸显。软件是新一代信息技术的灵魂&#xff0c;已经成为数字中国、制造强国、网络强国建设的关键支撑。面对全球竞争新格局&#xff0c;关键软件自主创新与国产化替代已迫在眉睫。 助力华为成功替换国外ERP系统 在此背景下…

android studio Switch按钮

1.添加按钮 <LinearLayoutandroid:layout_width"match_parent"android:layout_height"wrap_content"android:orientation"horizontal"><TextViewandroid:id"id/tv1"android:layout_width"0dp"android:layout_weig…

JavaScript如何实现继承?

&#x1f4dd;个人主页&#xff1a;爱吃炫迈 &#x1f48c;系列专栏&#xff1a;JavaScript &#x1f9d1;‍&#x1f4bb;座右铭&#xff1a;道阻且长&#xff0c;行则将至&#x1f497; 文章目录 继承JavaScript如何实现继承&#xff1f;原型链继承构造函数继承组合继承原型式…

纽扣电池出口欧盟ce认证EN62133测试项目

纽扣电池CE证办理&#xff0c;锂电CE证旨在提高环境性能的2006/66/EC入了电池和 蓄电池中0.0005%汞和便携式电池和蓄电池中0.002%镉的限值。自2013/56/EU 修订了2006/66/EC&#xff0c;2013/56/EU(修订2006/66/)规定&#xff0c;2015年10月1日 起&#xff0c;纽扣电池中汞的…