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

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

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

现在,我做了一些挖掘,我find了 。 在这里,似乎我应该这样写:

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

但是,这给我语法错误。 我已经尝试了各种各样的排列,但在所有情况下,Node.js似乎都不满意。

本质上,我想知道以下几点:

  1. Node.js做或不做TCO?
  2. Node.js中这个神奇的yield是如何工作的?

这里有两个相当不同的问题:

  • Node.js做或不做TCO?
  • Node.js中这个神奇的产量是如何工作的?

Node.js做或不做TCO?

TL; DR :现在不再是了,截至节点8.x。 它已经做了一段时间,落后于一个国家或另一个国家,但截至本文(2017年11月),它不再是因为它使用的底层V8 JavaScript引擎不再支持TCO。 看到这个答案更多的。

细节:

尾部呼叫优化(TCO)是ES2015(“ES6”)规范的必需部分 。 所以支持它并不是一个NodeJS的东西,而是NodeJS使用的V8 JavaScript引擎需要支持的东西。

从节点8.x开始,V8不支持TCO,甚至不支持标志。 它可能会在未来某个时候再次发生。 看到这个答案更多的。

节点7.10至less低于6.5.0(我的注意事项是6.2,但node.green不同意)仅在严格模式下支持标志后面的TCO(6.6.0及更高版本中的–harmony ,– harmony_tailcalls )。

如果你想检查你的安装,这里是node.green使用的testing(如果你使用相关的版本,一定要使用标志):

 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()); 
 $#只有特定版本的Node,特别是不是8.x或(当前)9.x; 往上看
 $ node --harmony tco.js
真正
真正

Node.js中这个神奇的yield是如何工作的?

这是另外一个ES2015的东西(“发电机function”),所以这也是V8必须实现的。 它完全在Node 6.6.0的V8版本中实现(并且已经有好几个版本),并没有落后于任何标志。

生成器函数(用function*和使用yield编写的function* )能够停止并返回捕获其状态的迭代器,并且可以用于在随后的场合继续其状态。 Alex Rauschmeyer在这里有一篇关于他们的深度文章。

下面是一个使用显式生成器函数返回的迭代器的例子,但是你通常不会这么做,而且我们马上就会明白为什么:

 "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()通过第一个yield语句运行代码。 此时, counter暂停并存储其内部状态。 it.next()返回一个具有done标志和value的状态对象。 如果done标志为false ,则该valueyield语句产生的值。
  • 每次调用it.next()counter内部的状态提前到下一个yield
  • 当调用it.next()使counter完成并返回时,我们返回的状态对象被设置为true并将value设置为counter的返回值。

具有迭代器和状态对象的variables以及调用it.next()以及访问donevalue属性都是(通常)阻碍我们尝试做的所有样板,所以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.valuefor-of done所有的it.next()调用并done对我们的检查。

node.js自2016.05.17 版本开始支持TCO。

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

6.2.0 – 用'严格'和'–harmony_tailcalls'

只能使用10000次的小尾部优化recursion(就像在问题中一样),但是失败的函数本身调用99999999999999次。

7.2.0用'严格'和' – 和谐'

国旗可以无缝,快速地与99999999999999的电话打交道。

更简洁的答案…截至实施date,如上所述…

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。

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

Interesting Posts