Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ES6中的尾调优化及其他相关的优化算法 #10

Open
uolcano opened this issue Sep 5, 2016 · 0 comments
Open

ES6中的尾调优化及其他相关的优化算法 #10

uolcano opened this issue Sep 5, 2016 · 0 comments

Comments

@uolcano
Copy link
Owner

uolcano commented Sep 5, 2016

前段时间看阮一峰老师的ES6入门的“函数的扩展”部分,发现几个这门语言内置的一些性能优化的功能和算法,觉得挺有意思,想自己试试顺便也总结一下,但是一直没时间,今天抽空来写一写。

尾调优化(Tail-call optimization)

尾调优化是为了避免不断保留和创建新的调用栈,而在函数最后一步调用另一个函数。这是ES6才开始出现的概念,常用于尾递归。
最后一步的意义就在于:不需要保留当前函数的执行环境(阮老师的原文讲的是调用帧"call frame",但我的理解是执行环境另一种说法),在调用的下一个函数执行完毕并给出返回值后,直接再返回,类似于pipe。wiki

尾调优化有很多表现形式:

  1. 直接作为函数调用

    function foo(x) {
        return x;
    }
    function bar(y) {
        return foo(y + 1);
    }
    bar(12);
  2. 对象方法调用

    var obj = { foo: function(x){ return x; } };
    function bar(y) {
        return obj.foo(y + 1);
    }
    bar(12);
  3. 通过函数原型的call或者apply方法来调用函数

    function foo(x) {
        return x;
    }
    function bar1(y) {
        return foo.call(null, y + 1);
    }
    function bar2(y) {
        return foo.apply(null, [y + 1]);
    }
    bar1(12);
    bar2(12);
  4. 条件操作符、逻辑操作符和逗号操作符

    function t(x){ return x; }
    function f(x){ return x; }
    function cond (y) {
        return y > 0 ? t(y + 1) : f(y - 1); // 两个函数都是尾调
    }
    function logiAnd (y) {
        return t() && f(); // 只有f()是尾调
    }
    function logiOr (y) {
        return f() || t(); // 只有t()是尾调
    }
    function comma (y) {
        return (f(), t()); // 只有t()是尾调
    }

    以上4个操作符简写的尾调形式可以分别等价于如下四个:

    function cond (y) {
        if(y > 0) {
            return t(y + 1);
        } else {
            return f(y - 1);
        }
    }
    function logiAnd (y) {
        if(!t()) {
            return f();
        }
    }
    function logiOr (y) {
        if(!f()) {
            return t();
        }
    }
    function comma (y) {
        f();
        return t();
    }
  5. 语句中的尾调优化

    暂未测试,详情见文末参考链接

尾递归(Tail-recursion)

尾递归就是利用尾调优化的特性,从语言机制上进行递归操作的优化,防止堆栈溢出(stack overflow)。

严格模式[有待考证]

好几篇文章都说:尾调优化只有在严格模式下才有效。但是实际上,只不过是argumentscaller不能使用。在chrome和Firefox上测试是否使用严格模式并没有影响到尾调优化的迹象。而且在严格模式下使用ES6的函数默认参数反倒会抛出错误Uncaught SyntaxError: Illegal 'use strict' directive in function with non-simple parameter list

尾递归优化实例

递归优化,在其他语言——比如C语言,就有递归转迭代的优化,而且递归和迭代是可以相互转换的,但是可能要牺牲空间复杂度,来换取更小的时间复杂度。这个时间复杂度就是递归优化的目的之一。

常见的递归实例

  1. 求自然数阶层:

    function factorial (n) {
        return n === 1 ? 1 : n * factorial(n - 1);
    }

    经过尾调优化后:

    function factorial (total, n) {
        return n === 1 ? total : factorial(n * total, n - 1);
    }
    factorial(1, 5); // 120
    factorial(1, 10); // 3628800
    factorial(1, 100); // 9.332621544394418e+157
    factorial(1, 1000); // Infinity
  2. 求斐波那契数值:

    function fibonacci (n) {
        return n <= 1 ? 1 : fibonacci(n - 1) + fibonacci(n - 2);
    }
    fibonacci(40); // 165580141
    // 到n的值为40,浏览器就已经响应很慢了,更大的数值直接就崩了。

    经过尾调优化后:

    function fibonacci (n, ac1, ac2) {
        (ac1 = ac1 || 1), (ac2 = ac2 || 1);
        return n <= 1 ? ac2 :fibonacci(n - 1, ac2, ac1 + ac2);
    }
    fibonacci(100); // 573147844013817200000
    fibonacci(1000); // 7.0330367711422765e+208
    fibonacci(10000); // Infinity

    经过尾调优化后,每次递归不需要保存其执行环境,只需要将一个末端的递归执行的返回值,逐层返回即可。这样就降低了内存占用,避免了堆栈溢出。

从以上两个例子可以看出,尾调优化后的递归,获取返回值的逻辑是逆向的。

尾调优化后再封装

factorial函数的优化版,多了一个前置参数,但是往往不需要每次输入的,所以可以改写一下,封装起来。

将上面尾调优化后的函数名改为tailFactorial

function tailFactorial (total, n) {
    return n === 1 ? total : tailFactorial(n * total, n - 1);
}
  1. 函数内部调用

    function factorial (n) {
        return tailFactorial(1, n);
    }
  2. 函数柯理化(currying)

    function curry (fn, args) {
        if(!isArray(args)) args = [args];
        return function (params) {
            if(!isArray(args)) params = [params];
            return fn.apply(this, args.concat(params));
        };
    }
    var factorial = curry(tailFactorial, 1);
  3. ES6函数参数默认值

    function factorial (n, total = 1) {
        if(n === 1) return 1;
        return n * factorial(n - 1, n * total);
    }

递归优化的实现

递归的迭代实现

递归优化的最佳实现应该是迭代。这里可以看到几种递归优化的对比表。

对上述factorialfibonacci函数转换为迭代实现。

function factorial (n) {
    var fact = 1;
    while (n > 1) {
        fact *= n--;
    }
    return fact;
}
function fibonacci (n) {
    var ac1, ac2, tmp,
        i = ac2 = ac1 = 1;
    while(n > i++) {
        (tmp = ac2),
        (ac2 = ac1 + ac2),
        (ac1 = tmp);
    }
    return ac2;
}

从以上代码可以看出:递归的迭代实现,跟递归实现的逻辑完全不同了;并且,每个递归迭代实现必须单独手动实现,没有统一的实现方式或辅助实现方式。

尾递归优化的实现

注意: 蹦床函数和尾调优化函数的尾递归优化实现,是在部分不支持尾调优化的情况下的手动实现。

  1. 蹦床函数(trampoline)

    顾名思义,蹦床函数就是随着递归执行的开始和结束,调用栈会出现入栈、出栈效果,就像是在弹蹦床。

    在使用蹦床函数辅助递归时,每次递归执行时都会保留上一次递归的激活对象(执行环境中的变量对象)的引用,执行本次递归完毕后,返回另一个待执行的递归,然后对上一次递归的激活对象的引用也就结束了。

    function trampoline (fn) {
        while (fn && fn instanceof Function) {
            fn = fn();
        }
        return fn;
    }
    
    function factorial (total, n) {
        return n === 1 ? total : () => factorial(n * total, n - 1);
    }
    
    function fibonacci (n, ac1 = 1, ac2 = 1) {
        return n <= 1 ? ac2 : () => fibonacci(n - 1, ac2, ac1 + ac2);
    }

    可以看到其实就是把前面的尾递归改为,返回一个绑定了下一次递归参数的匿名函数。

  2. 尾调优化函数

    实际上,蹦床函数并非真正的尾递归优化,以下才是:

    function tail (fn) {
        var value,
            active = false,
            stack = [];
        return function () {
            stack.push(arguments);
            if(!active) {
                active = true;
                while (stack.length) {
                    value = fn.apply(this, stack.shift());
                }
                active = false;
                return value;
            }
        };
    }
    
    var factorial = tail((total, n) => {
        return n === 1 ? total : factorial(n * total, n - 1);
    });
    factorial(1, 5); // 120
    
    var fibonacci = tail((n, ac1 = 1, ac2 = 1) => {
        return n <= 1 ? ac2 : fibonacci(n - 1, ac2, ac1 + ac2);
    });

    上述tail函数的精妙之处在于,第一次调用返回的匿名函数(使用时分别赋值给了factorialfibonacci)时,变量active会“激活”,导致后续每次要进一步递归时都不成功,返回值都是undefined,但是所有这些参数都被推入了stack数组。

    因此,在第一次调用后,每次执行递归,都只是进入递归将这次递归接受的参数列表推入stack数组,直接返回而不进入下一轮递归;而返回以后由于stack数组里有一个数组项,通过while循环又处理新的参数列表,所以就会一直这样“进入递归->获得参数列表->返回->进入递归->...”的轮回,直到某轮递归没有向stack数组推入参数。推入的参数在传入每轮递归时都会变化。

总结

尾调优化,实际上就是指在函数内部,调用另一个函数得到的返回值,不用进行其他操作,直接当做自己的返回值返回的特殊情况。

收获

源码可以查看我的Gist

  1. 柯理化函数

    function isArray (arg) {
        return Object.prototype.toString.call(arg).slice(8, -1) === 'Array';
    }
    function curry (fn, args) {
        if(!isArray(args)) args = [args];
        return function (params) {
            if(!isArray(args)) params = [params];
            return fn.apply(this, args.concat(params));
        };
    }
  2. 蹦床函数

    function trampoline (fn) {
        while (fn && fn instanceof Function) {
            fn = fn();
        }
        return fn;
    }
  3. 尾调优化函数

    function tail (fn) {
        var value,
            active = false,
            stack = [];
        return function () {
            stack.push(arguments);
            if(!active) {
                active = true;
                while (stack.length) {
                    value = fn.apply(this, stack.shift());
                }
                active = false;
                return value;
            }
        };
    }

参考链接


tags: tail-call optimization, tail-recursion

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant