阅读 188

ES6尾调用优化

这是我参与更文挑战的第10天,活动详情查看: 更文挑战

写在前面

ES6中函数比较有趣的变化有尾调用系统的引擎优化。尾调用是指函数作为另一个函数的最后一条语句被调用,就像这样

function doSomething() {
    return doSomethingElse();
}
复制代码

ES5中在执行 doSomethingElse 函数的时候会新增一个栈帧,doSomething对应的栈帧会被保留在内存中,当存在循环调用时就容易出现程序问题,比如栈溢出等不可预期的错误。

ES6中尾调用

ES6中的尾调用优化的条件

  • 严格模式下。
  • 尾调用不访问当前栈帧的变量(也就是说函数不是一个闭包)
  • 在函数内部,尾调用是最后一条语句。
  • 尾调用的结果作为函数值返回。

如果满足以下条件,尾调用不再创建新的栈帧,而是清除并重用当前栈帧;

"use strict";

// 可以被优化
function doSomething(){
    //优化后
    return doSomethingElse();
}

// 无法优化,必须在返回值后添加其他操作
function doSomething(){
    return 1+doSomethingElse();
}

// 无法优化,调用不在尾部
function doSomething(){
    var result = doSomethingElse();
    return result;
}

// 无法优化,func为闭包
function doSomething(){
    var num=1, func = () => num;
    return func();
}

复制代码

尾调用的案例

该代码主要的逻辑是实现阶乘的功能


function factorial(n) {
    if (n <= 1) {
        return 1;
    } else {
        return n * factorial(n - 1);
    }
}
复制代码

由于在递归调用前执行了乘法,所以无法被优化;如果n为一个较大的数,则会出现栈溢出的可能;

'use strict';
function factorial_opt(n, p=1) {
    if (n <= 1) {
        return 1 * p;
    } else {
        let result = n * p
        return factorial_opt(n - 1, result)
    }
}
复制代码

优化之后代码使用一个参数保存上一次的执行结果(每次调用会都会计算乘法的结果),不需要额外的函数调用,同时也符合被优化的特点;

验证结果 (一)

打开Google浏览器的 console 运行代码

image.png

似乎并非我们所想 打开node测试代码

$ node -v
v10.24.1

$ node foo.js
/Users/shuai/workspace/foo.js:2
function factorial_opt(n, p=1) {
                      ^
RangeError: Maximum call stack size exceeded
    at factorial_opt (/Users/shuai/workspace/foo.js:2:23)
    ...
复制代码

神奇的事情出现了 报错的信息与Google浏览器一致 优化的代码没有生效。

最终通过Google查询得到以下结果 转自 Github issue comment

V8 曾经实现过尾调用优化,但后来又放弃了,主要是因为实现尾调用优化意味着要在代码执行的过程中修改调用栈,这样的话,执行流的信息就不完整了,而且还会导致两个问题:

debug 的时候调用栈信息不完整 error.stack 的信息会不完整 鉴于这些原因,V8 团队建议修改规范,把 PTC(Proper Tail Calls) 改为 STC(Syntatic Tail Calls),也就是要用特定的语法来使用尾调用,比如 return continue func()。目前 V8 还是不支持的,曾经它提供了 --harmony-tailcalls 和 --harmony-explicit-tailcalls 来开启尾调用优化,不过后来又移除了。

验证结果 (二)

经过不懈的努力,最终找到 safari 支持尾调用的优化 image.png

最后的结果如下:

  • 未优化的执行结果

image.png

  • 优化后的执行结果

image.png

可以看到未经优化的代码依旧是会出现 RangeError: Maximum call stack size exceeded. 优化之后的代码正常执行, 而且运行效率很高。

斐波那契数列

// 一般的实现方式
function fibonacci(n) {
    if(n==0 || n == 1)
        return n;
    return fibonacci(n-1) + fibonacci(n-2);
}

// 尾调用优化后
'use strict'
function fibonacci_opt(n, current = 0, next = 1) {
    if(n <= 1) {
        return next
    }
    return fibonacci_opt(n - 1, next, next+current)
}

fibonacci_opt(5)
复制代码

总结

虽然尾调用优化是 ES6 规范中的一部分,但其实大部分 JS 引擎都没有实现。关于浏览器对于ES6规范的实现 可以查看 ES6在各大平台上的兼容性 这里只需要关注 Optimisation -> proper tail calls (tail call optimisation)。

在编码递归代码的时候,牢记尾调用的特点;在递归函数计算了足够大的情况下,尾调用将会大幅度优化程序性能。

文章分类
前端
文章标签