当前位置: 首页 > 科技观察

如何优化tailcall

时间:2023-03-18 19:25:35 科技观察

前言这里讲的是递归,这里就不赘述了。有兴趣的可以去查资料。要了解如何优化尾递归,我们需要从头开始。什么是尾调用什么是尾递归如何优化尾递归尾调用从字面上理解,很自然地在函数结束时返回一个函数调用。一般指函数执行的最后一步。比如👇constfn=()=>f1()||f2()//这里f2函数可能是尾调用,f1不能是尾调用,为什么f1函数不是,我们来看这个函数👇constfn=function(){constflag=f1()if(flag){returnflag}else{returnf2()}}的等效形式似乎写在这里。根据tailcall的定义,我们理解到最后只调用了f2个函数。说到这里,为什么要说tailcalls呢?让我们提前考虑一下传统的递归。通常先执行递归调用,然后根据递归的返回值来结算结果。那么传统递归的缺点是什么?👇效率低,占用内存。如果递归链太长,可能会导致栈溢出,那么我们可以优化一下吗?这可能涉及上面提到的尾调用。它的原理是什么?》根据阮一峰老师在es6中的函数扩展中的解释是:函数调用会在内存中形成一个“调用记录”,也称为“调用帧”(callframe),里面保存了诸如调用位置和内部变量,如果函数B在函数A内部被调用,那么在A对B的调用帧之上,也会形成B的调用帧,B运行结束后B的调用帧就会消失,结果返回给A,如果函数B还在内部调用函数C,则有另一个CFrame的调用,以此类推。所有的调用帧组成一个“调用栈”。这里的“调用帧”和“调用栈”应该是指到“执行环境”和“调用堆栈”。因为尾调用是函数的最后一个操作,所以不再需要保留外调用帧,而是直接替换外调用帧,这样可以起到优化的作用。从上面的描述我们可以了解到它的原理类似于当编译器检测到一个函数调用是尾递归时,会覆盖掉当前的activerecord,而不是在函数栈中创建一个新的调用记录。这样,我们也可以理解为不同语言的编译器或解释器都做了尾递归优化,使其不会爆栈。既然如此,尾递归的优化就要看浏览器了,主流浏览器有哪些支持呢?Safari和Firefox,有兴趣的可以去了解一下,可以自己写个斐波那契数列sequence验证一下。手动优化既然我们知道了很多浏览器不支持尾递归优化,那么大家一定很好奇,当我们使用尾递归优化的时候,仍然出现栈溢出错误,那我们该如何解决呢??👇在网上看到一个很好的解决方案,使用蹦床功能👇functiontrampoline(f){while(f&&finstanceofFunction){f=f();}returnf;}那么如何使用它👇让我们采用最常见的斐波那契数列functionfibonacci(n){if(n===0)return0if(n===1)return1returnfibonacci(n-1)+fibonacci(n-2)}根据上面的公式,我们可以把它写成迭代的形式,用一个变量来缓存它的值👇functionfibonacci(n,ac1=0,ac2=1){returnn<=1?ac2:fibonacci(n-1,ac2,ac1+ac2);}其实试过的人会发现当你需要的n很大的时候够了,还是会报错,类似下面的错误信息👇//fibonacci(10000)UncaughtRangeError:Maximumcallstacksizeexceeded此时,那么我们如何优化呢?真的没有办法解决吗👇这里不得不借鉴一下别人的思路,我觉得还不错,下面是代码👇functiontrampoline(f){while(f&&finstanceofFunction){f=f();}returnf;}你可以把这个函数称为蹦床函数,这个函数的作用是返回一个新的函数,如果我们把它们组合起来,栈的问题溢出似乎已经解决了👇//你可以在这里试试蹦床函数trampoline(fibonacci(10000))。我参考了别人的写法。好像这样写不太好。我个人认为这种方式可以避免调用栈溢出。实际情况下,并不是这样的,如果有不对的地方,希望大家指出。当然,手动优化可以将递归过程改写为迭代过程。以斐波那契数列题为例,我们可以使用动态规划来完成👇,O(n)来完成答案的更新。//伪代码F[i]=F[i-1]+F[i-2]嗯,将尾递归函数转换为循环迭代函数是一种手动优化的方式。我们的语言本身不支持尾递归优化,那么可以考虑这种情况。对于尾递归,我们需要了解优化它的原理。必要时,将递归形式写成迭代形式,通过迭代的方式减少重复值的计算。当然,这个过程有时是艰难的,值得深思。