Node.js尾调用优化:可能与否?

时间:2014-04-24 05:17:50

标签: javascript node.js tail-call-optimization

到目前为止,我喜欢JavaScript,并决定使用Node.js作为我的引擎,部分原因是this,声称Node.js提供了TCO。但是,当我尝试使用Node.js运行此代码(显然是尾部调用)时,会导致堆栈溢出:

function foo(x) {
    if (x == 1) {
        return 1;
    }
    else {
        return foo(x-1);
    }
}

foo(100000);

现在,我做了一些挖掘,然后找到了this。在这里,似乎我应该这样写:

function* foo(x) {
    if (x == 1) {
        return 1;
    }
    else {
        yield foo(x-1);
    }
}

foo(100000);

然而,这给了我语法错误。我尝试了各种各样的排列,但在所有情况下,Node.js似乎对某事不满意。

基本上,我想知道以下内容:

  1. Node.js做或不做TCO吗?
  2. 这个神奇的yield如何在Node.js中起作用?

4 个答案:

答案 0 :(得分:38)

这里有两个截然不同的问题:

  • Node.js做或不做TCO吗?
  • 这个神奇的产量如何在Node.js中起作用?
  

Node.js是否做TCO?

TL; DR 不再是,从Node 8.x 开始。它有一段时间,在一面旗帜或其他旗帜之后,但在撰写本文时(2017年11月)它已经不存在了,因为它使用的基础V8 JavaScript引擎不再支持TCO。有关详情,请参阅this answer

详细说明:

尾部呼叫优化(TCO)是必需的part of the ES2015 ("ES6") specification。因此,支持它不是一个NodeJS,它是NodeJS使用的V8 JavaScript引擎需要支持的东西。

从Node 8.x开始,V8不支持TCO,甚至不支持标志。它可能(再次)在未来的某个时刻;有关详情,请参阅this answer

至少节点7.10降至6.5.0(我的笔记说6.2,但node.green不同意)支持标志后面的TCO(--harmony在6.6.0及以上,--harmony_tailcalls更早)仅在严格模式下。

如果您想检查安装,以下是node.green使用的测试(如果您使用的是相关版本,请务必使用该标记):

function direct() {
    "use strict";
    return (function f(n){
      if (n <= 0) {
        return  "foo";
      }
      return f(n - 1);
    }(1e6)) === "foo";
}

function mutual() {
    "use strict";
    function f(n){
      if (n <= 0) {
        return  "foo";
      }
      return g(n - 1);
    }
    function g(n){
      if (n <= 0) {
        return  "bar";
      }
      return f(n - 1);
    }
    return f(1e6) === "foo" && f(1e6+1) === "bar";
}

console.log(direct());
console.log(mutual());
$ # Only certain versions of Node, notably not 8.x or (currently) 9.x; see above
$ node --harmony tco.js
true
true
  

这个神奇的yield如何在Node.js中运行?

这是ES2015的另一个东西(&#34;生成器功能&#34;),所以它也是V8必须实现的东西。它完全在节点6.6.0中的V8版本中实现(并且已经有多个版本)并且不在任何标志之后。

生成器函数(使用function*并使用yield编写的函数)通过​​能够停止并返回捕获其状态的迭代器来工作,并可用于在后续场合继续其状态。 Alex Rauschmeyer有一篇关于他们的深度文章here

这是一个明确使用生成器函数返回的迭代器的示例,但您通常不会这样做,我们会在一瞬间看到原因:

"use strict";
function* counter(from, to) {
    let n = from;
    do {
        yield n;
    }
    while (++n < to);
}

let it = counter(0, 5);
for (let state = it.next(); !state.done; state = it.next()) {
    console.log(state.value);
}

有这个输出:

0
1
2
3
4

这是如何运作的:

  • 当我们调用counterlet it = counter(0, 5);)时,初始化对counter的调用的初始内部状态,我们立即返回迭代器; counter中没有任何实际代码运行(尚未)。
  • 调用it.next()会在counter中运行第一个yield语句中的代码。此时,counter暂停并存储其内部状态。 it.next()返回一个状态对象,其中包含done标记和value。如果done标记为false,则valueyield语句产生的值。
  • 每次调用it.next()都会将counter内的状态提升到下一个yield
  • 当对it.next()的调用使counter完成并返回时,我们返回的状态对象将done设置为true并将value设置为返回值counter

拥有迭代器和状态对象的变量,并调用it.next()并访问donevalue属性,这些都是(通常)妨碍我们和#39;重新尝试这样做,所以ES2015提供了新的for-of语句,它将我们所有的东西都收起来,只给我们每个值。以上用for-of编写的相同代码:

"use strict";
function* counter(from, to) {
    let n = from;
    do {
        yield n;
    }
    while (++n < to);
}

for (let v of counter(0, 5)) {
    console.log(v);
}

v与我们上一个示例中的state.value相对应,for-of为我们执行了所有it.next()次呼叫和done次检查。

答案 1 :(得分:4)

node.js最终支持自2016.05.17起的TCO,version 6.2.0

需要执行--use-strict --harmony-tailcalls标志才能使TCO正常工作。

答案 2 :(得分:0)

  

6.2.0 - 使用'use strict'和'--harmony_tailcalls'

仅适用于小尾部优化的10000递归(如问题中所示),但函数调用本身失败99999999999999次。

  

7.2.0使用'use strict'和'--harmony'

即使使用99999999999999,

flag也能无缝快速地工作。

答案 3 :(得分:-1)

更简洁的答案......截至发布之日,如上所述......

TCO有效。它不是防弹的,但它非常体面。这是因子(7000000,1)。

>node --harmony-tailcalls -e "'use strict';function f(n,t){return n===1?t:f(n-1,n*t);}; console.log(f(7000000,1))"
Infinity

这里没有TCO。

>node -e "'use strict';function f(n,t){return n===1?t:f(n-1,n*t);}; console.log(f(15669,1))"
[eval]:1
function f(n,t){return n===1?t:f(n-1,n*t);}; console.log(f(15669,1))
      ^

RangeError: Maximum call stack size exceeded
at f ([eval]:1:11)
at f ([eval]:1:32)
at f ([eval]:1:32)
at ...

它确实让它一直到15668。

关于产量,请参阅其他答案。应该是一个单独的问题。