程序设计中的计算复用(Computational Reuse)

学习提速专享!AI工具全家桶免费用 限时购周边加赠Coding Plan Lite,解锁20+主流AI工具,写代码、查资料快人一步! 阅读详情

转载自:张洋的blog

从斐波那契数列说起

我想几乎每一个程序员对斐波那契(Fibonacci)数列都不会陌生,在很多教科书或文章中涉及到递归或计算复杂性的地方都会将计算斐波那契数列的程序作为经典示例。如果现在让你以最快的速度用C#写出一个计算斐波那契数列第n个数的函数(不考虑参数小于1或结果溢出等异常情况),我不知你的程序是否会和下列代码类似:

public staticulong Fib(ulongn)
{
    return(n == 1 || n == 2) ? 1 : Fib(n - 1) + Fib(n - 2);
}

这段代码应该算是短小精悍(执行代码只有一行),直观清晰,而且非常符合许多程序员的代码美学,许多人在面试时写出这样的代码可能心里还会暗爽。但是如果用这段代码试试计算Fib(100)我想就再也爽不起来了,估计下星期甚至下个月前结果很难算得出来。

看来好看的代码未必中用,如果程序在效率不能接受那美观神马的就都是浮云了。如果简单分析一下程序的执行流,就会发现问题在哪,以计算Fibonacci(5)为例:

image

从上图可以看出,在计算Fib(5)的过程中,Fib(1)计算了两次、Fib(2)计算了3次,Fib(3)计算了两次,本来只需要5次计算就可以完成的任务却计算了9次。这个问题随着规模的增加会愈发凸显,以至于Fib(100)已经无法再可接受的时间内算出。虽然可以通过尾递归优化将双递归变为单递归,但是效果也并不理想。

这是一个非常典型的忽视“计算复用”的例子。计算复用的目标在于保证计算过程中同一计算子过程只进行一次,通过保存子过程计算结果并复用来提高计算效率。其实类似上面的代码出现在很多教科书中,如果是为了展示斐波那契数列的数学特性当然无可厚非,但是作为计算机程序就很有问题了。因为数学和计算科学是有区别的,数学要求严谨和简洁的表达,而计算科学则需要尽量快的得出结果,好的数学公式未必是好的计算公式。这也说明程序设计不是简单的将数学语言翻译为计算机语言就可以了,程序员应该能将数学语言首先翻译成计算科学语言(算法?),然后再翻译成机器语言。因此程序员的工作绝不是机械的,而是要有一定的创造性,所以必要的算法知识对程序员至关重要,因为算法教会程序员如何用最有效率的方式去编写程序。

言归正传,根据以上分析,可以写出一个更高效的斐波那契数列计算程序:

public static ulong Fib(ulong n)
{
    if (n == 1 || n == 2)
    {
        return 1;
    }
    ulong m1 = 1, m2 = 1;
    for (ulong i = 3; i <= n; i++)
    {
        m2 = m1 + m2;
        m1 = m2 - m1;
    }

    return m2;
}


这段代码可能看起来不如上一段那么优美,但是其效率却是第一段代码不可比拟的。例如计算Fib(40),在我的机器上,第一段代码用时3.5秒,而第二段代码小于0.001秒。这个差距随着规模增大会更明显,例如Fib(100),第一段代码可能需要几天甚至几周,而第二段代码耗时仍然小于0.001秒。天壤之别!

如果从计算复杂性的角度分析,第一段代码的复杂度为O(1.6^n),对数学敏感的朋友应该能体会到这个函数可怕的增长速度,这甚至不是一个多项式级别的复杂度,而第二段代码仅为O(n)。看到如此简单一个例子出现如此差别,还能说程序员学习算法没有用吗。

上面代码对于“计算复用”的思想体现不是很明显,因为我们仅仅需要一个结果,中间结果都被丢弃了,如果是计算1<=i<=n的所有Fib(i),那么计算复用的思想就会体现的比较明显。

矩阵乘法与Strassen算法

下面说一个将计算复用发挥到极致的例子,说实话直到现在每次看到Strassen算法我都觉得震撼,不知Strassen当年是长了何等天才的脑子才发现这么漂亮的一个算法。

矩阵计算在许多领域如机器学习、图形图像处理、模式识别中均占有重要地位。而计算两个n*n矩阵乘积的运算是矩阵计算中常见的计算。由矩阵理论可知,普通方法计算两个n阶方阵的乘积需要进行n^3次乘法计算,其计算复杂度自然是O(n^3)。但是德国数学家Volker Strassen通过拆分矩阵并复用计算结果,发现了一种复杂度为O(n^2.81)的算法,这个算法简单说来如下。

假设n为2的幂(不为2的幂也能计算,这里是为了方便说明),A和B是两个n阶方阵,则A和B分别可以分解成4个n/2阶方阵:

则:

可惜这样经过8次n/2阶方阵相乘,复杂度还是O(n^3),没有降低复杂度。天才的Volker Strassen发现了一种通过计算7次n/2阶方阵来得出n阶方阵乘积的方法。具体来说,假设每个矩阵的积可以写成如下形式:

然后设:

这样通过7次n/2矩阵的相乘计算出P1-P7,然后:

这样就组合出了AB,这个方法的复杂度为O(n^2.81),这个算法实在是太漂亮了。天才!绝对的天才啊!对于这种人除了无限崇敬我真是没有其它想法了,能将计算复用发挥到如此境地,不知世间能有几人。

计算复用对软件开发的启示

也许有的朋友会说,“我又不开发数值计算型程序,也不会接触如此复杂的算法,计算复用与我何干?”。实际上即使开发非数值型程序,计算复用的思想也是大有用途的。例如我曾经在一个真实的PHP开发的行业系统中见过类似这样的代码:

1foreach($itemsas $k => $v){
2    //...
3    $money= $v->money + getTax();
4    //...
5}

当时我问开发这个程序的人这里getTax的返回值和每个item有关系吗,他说税费是一套复杂的算法算出来的,但是其值固定的。那这里可就太浪费了,每次循环都计算一次,如果改为如下:

1$tax = getTax();
2foreach($itemsas $k => $v){
3    //...
4    $money= $v->money +$tax;
5    //...
6}

则可以节省不少计算资源。在后来的沟通中发现这个问题原来是重构的遗留问题,以前系统中的税率计算是写在程序里的,后来发现这个计算越来越多,就使用“Extract Method”重构模式提取成了getTax函数,但是这样的后果就是到处都是getTax调用,有的程序段甚至调用七八次,但是如果应用计算复用的思想,则应该在脚本开始只计算一次税费并保存,后面全都使用这个变量而不是每次调用getTax。

总之,只要某个计算结果与执行上下文无关,并且在一个执行流中超过一次被使用,则应该使用计算复用。

这个例子还算明显的,有时可能不会这么明显,例如我们知道JavaScript中从深层函数中引用全局对象的代价是很高的,因为需要遍历作用域链(当然是隐式的),因此在JS中如果深层函数代码频繁使用全局对象,则要付出很高的代价。如果程序员不懂得对象及作用域链相关知识,则不会发现这种潜在的效率问题,而正确的做法是使用一个局部变量保存对全局对象的引用而不是每次都直接使用全局变量。

很多成熟的产品也处处体现着计算复用的思想,如在PHP中,下面代码可以得到一个数组的元素个数:

1echo count($arr);

如果我们来实现,最自然的想法就是遍历数组。但是PHP的开发者明显更聪明,他们在建立数组时同时建立一个与之关联的内部的数量计数变量(对PHP程序员透明),随着数组元素的增减,这个变量也相应增减,每次调用count函数直接返回这个变量即可,这就将count的复杂度从O(n)降为O(1),这也是计算复用的一个典型应用。

另外,其实计算复用和缓存的概念是相通的,很多缓存系统就使用了计算复用的思想。



软件项目工作量估算之软件复用分析 软件工作量估算是我们在对一个软件项目成本估算过程中,在完成软件规模估算后需要进行的第2项估算工作。在软件项目工作量估算过程中,我们除了要进行风险分析外,还要考虑软件复用的程。 软件复用就是将已有的软件成分用于构造新的软件系统。在现代的软件开发过程中,为了提高效率和质量,大部分软件企业都已将某些通用功能转化为可重用功能,或者开发组织具备某方面项目的开发经验,遗留下了可以复用的组件,这些情况都可能降... 阅读详情

相关推荐

密码学理论03:计算安全性(Computational Security)

如果对于每个 PPT 对手 A 执行某种正式指定类型的攻击,并且对于每个正多项式 p,存在一个整数 N,使得当 n > N 时,A 攻击成功的概率小于1/p(n),则该方案是安全的。

weixin_51271794的博客 1972

一种软件复用量的新方法

基于COSMIC-FFP 方法和面向对象需求模型,总结两者元模型之间的映射关系,从用户功能的角提出功能复用的基本思想,分 析出需求模型中功能复用可能存在的3 种泛化关系,并给出基于用例的功能需求计算方法,同时引入功能复用指标来计算用户功能需求总 数中被复用的FUR 的比例。以实例说明面向对象系统中功能复用量的过程

Journal of Computational Physics LaTeX模板终极配置指南(附常用扩展包清单)

本文提供了Journal of Computational Physics LaTeX模板的终极配置指南,涵盖从基础设置到高级扩展包的详细教程。针对计算物理论文的特点,重点介绍了数学公式增强、图表处理、算法代码展示等实用技巧,并分享了常见问题排查与性能优化方案,帮助研究者高效完成符合期刊要求的论文排版。

t4y5u6i7o的博客 526

软件复用是什么意思

软件复用是将已有的软件及其有效成分用于构造新的软件或系统。它不仅是对软件程序的复用,还包括对软件生产过程中其它劳动成果的复用,如项目计划书、可行性报告、需求分析、概要设计、详细设计、编码(源程序)、测试用例、文档与使用手册等等。因此,软件复用包括软件产品复用和软件过程复用两部分的内容。 软件复用不同于软件移植。软件移植是指对软件进行修改和扩充,使之在保留原有功能、适应原有平台的基础上,可以运行于新

waiyyn的专栏 4607

软件复用技术

软件复用(SoftWare Reuse)是将已有软件的各种有关知识用于建立新的软件,以缩减软件开发和维护的花费。软件复用是提高软件生产力和质量的一种重要技术。早期的软件复用主要是代码复用,被复用的知识专指程序,后来扩大到包括领域知识、开发经验、设计决定、体系结构、需求、设计、代码和文档等一切有关方面。软件复用是一种计算机软件工程方法和理论。60年代的“软件危机”使程序设计人员明白难于维护的软

风往哪吹~ 1456

软件开发费用评估 功能点估算法

最近写项目文档,需要计算报价,用到了功能点估算法,还挺复杂的,姑且做个笔记。 定制软件开发服务费可按照功能点估算法或工作量估算法进行估算,原则上200万元以上项目需按功能点法估算。具体公式如下: 定制软件开发服务费用=功能点数×软件开发生产率基准/人月折算系数×软件开发基准人月费率+直接非人力成本 其中: 1)功能点数(Function Point) 单位为个,计算公式如下: 功能点数=未调整功能点数量(UFP)×软件类别调整因子×复用系数。 未调整功能点数量(UFP)可采用预估功能点计数和估

Boss_Gao的博客 4万+

从零手搓旋转目标检测:角表示、回归与边界问题实战

目标检测是计算机视觉的基础任务,但传统水平框在面对细长、倾斜目标时,会引入大量背景区域,导致定位精下降、NMS误杀频发。旋转目标检测通过引入角自由,以带方向的旋转框精准贴合目标主体,成为遥感、工业质检等场景的关键技术。其核心在于旋转框的数学表示、角回归的周期性处理以及旋转NMS等后处理细节。理解这些底层原理,才能在实际工程中灵活应对数据格式不统一、Loss爆炸、部署适配等问题。本文基于PyTorch,从数学定义出发,不借助封装库,逐步构建一个最小可运行的旋转目标检测闭环,并重点剖析角边界跳变这一训

weixin_34122604的博客 387

华为数字化转型必修课229页PDF核心拆解:从方法论到落地实践

数字化转型已成为企业提升竞争力的关键路径,但许多组织在推进过程中常陷入技术堆砌而忽视管理变革的困境。理解转型的本质,需要从业务痛点、流程重构与数据治理等基础概念入手。企业架构作为连接战略与执行的桥梁,强调业务与IT的协同驱动,通过模块化拆解客户体验、业务流程、产品服务、管理体系与生态协同等维,形成可落地的转型作战地图。华为内部方法论的核心在于“三分技术、七分管理”,尤其重视数据治理的规范性与组织文化的配套支撑。本文基于一份229页的华为数字化转型必修课资料,梳理其六模块框架、冰山模型、双轮驱动原则及四步落

weixin_34026276的博客 423

斐波那契数列,计算复用(Computational Reuse)

0,1,1,2,3,5,8...这种数列大家都看过如果算出第N列的值得算法.其中广为流传最经典的为:publicstaticulongFib(ulongn){return(n == 1 || n == 2) ? 1 : Fib(n - 1) + Fib(n - 2);}许多人在面试时写出这样的代码可能心里还会暗爽。但是如果用这段代码试试计算Fib(100)我想就再也爽不起来了,...

weixin_30586085的博客 130

Computational-Finance-Course 使用教程

Computational-Finance-Course 使用教程 1、项目介绍 Computational-Finance-Course 是一个开源项目,旨在通过提供一系列的课程材料和代码示例,帮助学习者掌握计算金融领域的核心概念和技术。该项目基于书籍《Mathematical Modeling and Computation in Finance: With Exercises and Pyt...

gitblog_00717的博客 1038

计算图结构(Computational Graph)

计算图是一种用有向图节点(Node):代表基本运算单元(如加法、乘法、激活函数)或数据输入(如常数、变量、张量)边(Edge):表示数据流向与依赖关系,箭头指向运算结果的传递方向本质:将复杂数学表达式拆解为可执行的基本运算序列,使梯计算、并行优化和硬件加速成为可能。

m0_46322965的博客 96
上一篇: 电子钱包&电子存折,区别
下一篇: socket心跳机制so_keepalive设计三个参数详解
taolinke
博客等级 码龄19年 94粉丝 105原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值