为了性能,慎用递归

这篇具有很好参考价值的文章主要介绍了为了性能,慎用递归。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

慎用递归

起因:

在学习Rust的时候,有一道语法练习题是计算斐波那契数列的第N项的值,这是一道非常简单的题,但是引发了一个使用递归性能问题,考虑到用Rust的人不多,后面的代码都是C#的,因为C#的语法更大众一些,更好看懂

第一次解

public static ulong FibonacciNumberRecursion(int n)
{
    if (n == 1)
        return 0;
    else if (n == 2)
        return 1;
    else
    {
        return FibonacciNumberRecursion(n - 1) + FibonacciNumberRecursion(n - 2);
    }
}

这个写法非常的符合大脑思考,第一项返回0,第二项返回1,后面的返回前两项之和,简单测试没有任何问题。但是,这个算法有非常严重的性能问题,当n到40的时候,计算速度已经到了肉眼不可接受的地步,再往上就到了分钟级的了,造成运行缓慢的原因,就是递归会不停的压栈,存储当前的状态,这是非常没有必要的,于是我写了第二种解法

第二次解

public static ulong FibonacciNumber(int n)
{
    if (n == 1)
        return 0;
    else if (n == 2)
        return 1;
    else
    {
        var x = 3;
        ulong xSub1 = 1;
        ulong xSub2 = 0;
        ulong value = 0;
        while (x <= n)
        {
            value = xSub1 + xSub2;
            xSub2 = xSub1;
            xSub1 = value;
            x += 1;
        }
        return value;
    }
}

这一次使用循环代替递归,它没有频繁的压栈,性能非常好,计算第200项的值也在纳秒级别,于是便有了思考,是否所有的递归都是这么不堪?经过查阅资料,发现第一次的递归有很多是无效递归,于是进行了改写

第三次解

public static ulong FibonacciNumberRecursion2(int n, ulong a = 0, ulong b = 1)
{
    // 斐波那契数列是第N项等于前两项的和
    if (n == 1)
    {
        return a;
    }
    else
    {
        return FibonacciNumberRecursion2(n - 1, b, a + b);
    }
}

这一次的递归使用了a和b两个变量去缓存前两项的值,这里看起来和实际情况是有差异的,它的计算过程更接近循环,因为a,b是从0,1开始往上加出来的,虽然递归是n-1。这里的n-1更像是为了达到终止递归的条件

经过修改的递归方法,性能和循环已经很接近了,只差一点点,那这个是不是递归已经非常强了?也不是,经过查阅资料,发现是编译器针对尾递归进行了优化,会用类似循环的机制来运行尾递归

尾递归:如果一个函数中所有递归形式的调用都出现在函数的末尾,我们称这个递归函数是尾递归的。当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时,这个递归调用就是尾递归。尾递归函数的特点是在回归过程中不用做任何操作,这个特性很重要,因为大多数现代的编译器会利用这种特点自动生成优化的代码。

第四次解

经过上面的解法,经过编译器优化的尾递归已经很好了,但是还想看看如果没有优化的性能是什么样子呢?因为第一次解的速度慢不只是递归的原因,还有很多无意义计算,那么抛开无意义的计算,递归和循环有多少差距呢?

public static ulong FibonacciNumberRecursion3(int n, ulong a = 0, ulong b = 1)
{
    // 斐波那契数列是第N项等于前两项的和
    if (n == 1)
    {
        return a;
    }
    else
    {
        var r = FibonacciNumberRecursion3(n - 1, b, a + b);
        var z = r + 1;
        
        return z-1;
    }
}

在这里使用了+1和-1,主要是为了破坏尾递归,那么最后的性能是怎样的呢

BenchmarkDotNet v0.13.10, Windows 10 (10.0.19045.3570/22H2/2022Update)
AMD Ryzen 7 4800HS with Radeon Graphics, 1 CPU, 16 logical and 8 physical cores
.NET SDK 8.0.100
  [Host]     : .NET 6.0.25 (6.0.2523.51912), X64 RyuJIT AVX2
  DefaultJob : .NET 6.0.25 (6.0.2523.51912), X64 RyuJIT AVX2
Method Mean Error StdDev
Loop 53.02 ns 0.111 ns 0.098 ns
Recursion2 52.98 ns 0.261 ns 0.232 ns
Recursion3 348.34 ns 4.367 ns 4.084 ns

求第200项的值,Loop使用循环,Recursion2是尾递归,Recursion3是破环了尾递归的情况,从这上面来看,卫队贵对性能的影响还是很大的

结论

上面4中求斐波那契数列的第N项值的写法,有不同的性能表现,使用循环和尾递归相差无几,如果是线性递归,那么性能就会差很多,因此

为了性能,优先使用循环解决问题,经过编译器优化的尾递归性能也不差,尽量避免使用普通的递归文章来源地址https://www.toymoban.com/news/detail-746512.html

到了这里,关于为了性能,慎用递归的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包赞助服务器费用

相关文章

  • 告警:线上慎用 BigDecimal !

    来源:cnblogs.com/zhangyinhua/p/11545305.html Java在java.math包中提供的API类BigDecimal,用来对超过16位有效位的数进行精确的运算。双精度浮点型变量double可以处理16位有效数,但在实际应用中,可能需要对更大或者更小的数进行运算和处理。 一般情况下,对于那些不需要准确计算精度

    2024年02月08日
    浏览(7)
  • 【spark】dataframe慎用limit

    官方:limit通常和order by一起使用,保证结果是确定的 limit 会有两个步骤: LocalLimit ,发生在每个partition GlobalLimit,发生shuffle,聚合到一个parttion 当提取的n大时,第二步是比较耗时的 如果对取样顺序没有要求,可用tablesample替代,使用详解。 官方 Stop using the LIMIT clause wrong

    2024年02月10日
    浏览(10)
  • 花2个月时间整理了3.5W字的自动化测试面试题(答案+学习路线)!为了找到好工作,拼了!

    从5月初开始找工作到现在,先后面试了阿里巴巴、字节跳动、网易、快手的测试开发岗。 大公司对于测试开发的要求相比来说高很多,要求掌握的知识点的广度和深度层次也比较高,遂整理了这两个月的面试题目文档供大家参考,同时也是为了方便以后自己需要的时候刷一

    2024年02月09日
    浏览(10)
  • 掌握Go语言:探索Go语言递归函数的高级奥秘,优化性能、实现并发、解决算法难题(28)

    递归函数在Go语言中是一种强大的工具,能够解决许多复杂的问题。除了基本的递归用法外,Go语言还提供了一些高级用法,使得递归函数更加灵活和强大。本文将深入探讨Go语言递归函数的高级用法,包括尾递归优化、并发递归和记忆化递归等。 尾递归优化 尾递归是一种特

    2024年04月10日
    浏览(12)
  • 宿主机无法连接docker里的redis问题解决(生产环境慎用)

    1.连接超时 2.连接能连上但马上断开并报错 3.提示保护模式什么的 链接redis 时只能通过本地localhost (127.0.0.1)这个来链接,而不能用网络ip(192.168…)这个链接 1.打开配置文件把下面对应的注释掉 2.Redis默认不是以守护进程的方式运行,可以通过该配置项修改,使用yes启用守护进

    2024年04月13日
    浏览(19)
  • 排序算法大全:冒泡排序【含优化】,选择排序【含优化】,直接插入排序,希尔排序,堆排序,快速排序【含3种实现版本及非递归实现】,归并排序【含非递归实现】。详细图解,文字解释,代码实现,性能分析。

    排序算法大全:冒泡排序【含优化】,选择排序【含优化】,直接插入排序,希尔排序,堆排序,快速排序【含3种实现版本及非递归实现】,归并排序【含非递归实现】。详细图解,文字解释,代码实现,性能分析。

    目录  一、冒泡排序 1、冒泡排序思想 2、冒泡排序算法的性能分析 二、选择排序 1、选择排序思想 2、选择排序算法的性能分析   三、直接插入排序 1、直接插入排序思想 2、直接插入排序算法的性能分析 四、希尔排序 1、希尔排序思想 2、希尔排序算法的性能分析 五、堆排

    2024年02月20日
    浏览(13)
  • 递归算法学习——子集

    递归算法学习——子集

      目录 一,题目解析 二,例子 三,题目接口  四,解题思路以及代码 1.完全深度搜索 2.广度搜索加上深度优先搜索 五,相似题 1.题目 2.题目接口  3.解题代码 给你一个整数数组  nums  ,数组中的元素  互不相同  。返回该数组所有可能的子集(幂集)。 解集  不能  包含

    2024年02月10日
    浏览(8)
  • 算法学习 | 递归方程求解

    算法学习 | 递归方程求解

    目录 一、特征方程 2.1 线性齐次递推式的求解 2.1.1 对于一阶齐次递推关系 2.1.2 对于二阶齐次递推关系  2.2 非齐次递推式的求解 2.2.1 常用的非齐次递推式的求解1(ing) 2.2.2 常用的非齐次递推式的求解2(ing) 二、递归树 三、主方法 3.1 主定理 3.2 应用实例 用来代替该等式中的

    2024年02月09日
    浏览(13)
  • 递归算法学习——全排列

    递归算法学习——全排列

    目录 ​编辑 一,问题描述 1.例子: 题目接口:  二,问题分析和解决 1.问题分析 2.解题代码 首先我们得来先看看全排列的问题描述。全排列问题的问题描述如下: 给定一个不含重复数字的数组  nums  ,返回其  所有可能的全排列  。你可以  按任意顺序  返回答案。 1.例

    2024年02月11日
    浏览(10)
  • MybatisPlus 使用 saveOrUpdate 详解(慎用),及问题解决方法&mysql保存或更新 ON DUPLICATE KEY UPDATE

    MybatisPlus 使用 saveOrUpdate 详解(慎用),及问题解决方法&mysql保存或更新 ON DUPLICATE KEY UPDATE

    今天的想法是,要在插入数据库时,如果有某某一个主要字段的值重复,则不插入,否则则插入! 看了一下mybatis-Plus是有这个saveOrUpdate 方法! 原本使用save时是没有问题了,改成saveOrUpdate 用了一下就报错了。 com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: error: can not execut

    2024年02月11日
    浏览(17)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包