我的个人网站 www.ryzeyang.top
内容概览
通过chrome浏览器的性能分析工具 performance 分析 es6 的尾递归
尾调用优化
尾调用(Tail Call)是函数式编程的一个重要概念,本身非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数。
// 情况一 调用函数g之后,还有赋值操作,所以不属于尾调用
function f(x){
let y = g(x);
return y;
}
// 情况二 也属于调用后还有操作 所以不属于尾调用
function f(x){
return g(x) + 1;
}
// 情况三 最后一步为 return undefined; 所以不属于尾调用
function f(x){
g(x);
}
每次调用函数时,都会产生一个调用栈(会进行一个压栈的操作,栈的特点是先进后出,看看下面的例子),而尾调用是函数的最后一步操作,可以直接用内层函数的调用帧替换外层函数。
function getB(){ //第四步: 执行getB函数中的代码
return 'B'
}
function c(a,b){
return a+b
}
function foo() { //第二步: 执行foo函数中的代码
let a=1
let b=getB(); // 第三步: 调用 getB 函数 getB 进栈
return c(a , b); //第五步:调用c函数 c进栈 执行尾调用优化
}
foo(); //第一步:调用foo函数 foo进栈
代码流程浅析:
操作 | 栈 |
---|---|
1. 调用foo函数 | foo 进栈 |
2. 执行foo函数中的代码 | |
3. 调用 getB 函数 | getB 进栈 |
4. 执行getB函数中的代码 | |
5. getB 函数执行完 | getB 出栈 |
6. 调用 c 函数(尾调用) | 用c的栈帧替换foo的栈帧 |
7. 执行 c 函数中的代码 | |
8. c 函数执行完 | c 出栈 |
注意,只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则就无法进行“尾调用优化”
尾递归 ✓ (牛!)
函数调用自身,称为递归。如果尾调用自身,就称为尾递归。
递归时会容易造成这个stack overflow,但是尝试将代码改成尾递归之后,那个速度就是飞起了。
function Fibonacci (n) {
if ( n <= 1 ) {return 1};
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
if( n <= 1 ) {return ac2};
return Fibonacci2 (n - 1, ac2, ac1 + ac2);
}
console.time("Fibonacci(10) time");
Fibonacci(10) // 89
console.timeEnd("Fibonacci(10) time");
console.time("Fibonacci(20) time");
Fibonacci(20)
console.timeEnd("Fibonacci(20) time");
console.time("Fibonacci(40) time");
Fibonacci(40)
console.timeEnd("Fibonacci(40) time");
console.log('----------------------------')
console.time("尾递归优化过的Fibonacci2(10) time");
Fibonacci2(10)
console.timeEnd("尾递归优化过的Fibonacci2(10) time");
console.time("尾递归优化过的Fibonacci2(20) time");
Fibonacci2(20)
console.timeEnd("尾递归优化过的Fibonacci2(20) time");
console.time("尾递归优化过的Fibonacci2(1000) time");
Fibonacci2(1000)
console.timeEnd("尾递归优化过的Fibonacci2(1000) time");
通过chrome浏览器的性能分析工具 performance 可以得到下面这张图
可以明显地看出没有优化过的 Fibonacci 函数 在递归次数为10时 没优化过的 Fibonacci 比使用尾递归优化过的Fibonacci2 还快了0.02ms,超过10之后性能明显下降,左边的红框是Fibonacci(40)的执行结果,要花费2450.614990234375ms 而后边是Fibonacci2(1000)的执行结果 只要0.22412109375ms 这个性能真的差老远了 🙃
通过这个图,还可以明显地看到,右边的红框中Fibonacci2是笔直下来的,即Fibonacci2(1000) 的每次递归消耗的时间是没变的,而左边的这个Fibonacci 会有不同的长度, 说明它每次递归消耗的时间是不同的。
这个直接展示了这个尾递归的优势。(chrome也支持尾递归了,不止safari)