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

Posted

技术标签:

【中文标题】Node.js 尾调用优化:可能与否?【英文标题】:Node.js tail-call optimization: possible or not? 【发布时间】:2014-06-09 05:39:57 【问题描述】:

到目前为止,我喜欢 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 似乎都对 something 不满意。

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

    Node.js 有没有 TCO? yield 这个神奇的东西在 Node.js 中是如何工作的?

【问题讨论】:

运行带有--harmony 标志的节点,看看你的第二个版本是如何工作的。例如node --harmony mytest.js。但是首先重新查看您引用的示例,您只根据您的情况调整了其中的一部分。关于 TCO,真正的问题是 V8 是否已经实现了它——在我看到的 v8 changelog 中没有提到它已经完成。 @barry-johnson:我尝试在第二个链接中使用yield 复制示例函数,而Node.js 对function* 表示例外。这也是我困惑的原因之一。 这就是为什么我说你需要使用 --harmony 选项运行节点。生成器是 ES6/Harmony 的一部分,不是节点默认的。 【参考方案1】:

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

Node.js 有没有 TCO? 这个神奇的 yield 东西在 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());
$ # 仅限某些版本的 Node,特别是 8.x 或(当前)9.x;往上看 $节点--harmony tco.js 真的 真的

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

这是另一个 ES2015 的东西(“生成器函数”),所以这也是 V8 必须实现的东西。它在 Node 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 属性都是(通常)妨碍我们尝试做的所有样板,所以 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 执行所有 it.next() 调用,done 为我们检查。

【讨论】:

请把 和同时放在同一行 - 我似乎更清楚 - 无法编辑 - 因为更改 2 个字符不足以进行编辑... @AndyS:纯粹的风格编辑在任何情况下都不合适。【参考方案2】:

node.js 终于从 2016.05.17 开始支持 TCO,version 6.2.0.

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

【讨论】:

链接的页面在任何地方都没有“tail”或“TCO”,您可以链接到宣布支持尾调用的内容吗? (支持那里,我已经检查过了。)另外请注意,启用它不需要--use-strict,只需--harmony-tailcalls 另请注意,它背后的原因是 V8 团队认为它不稳定,更不用说完成了。他们仍然(从 Node v6.2.2 中的 V8 开始)认为它正在进行中 @T.J.Crowder:虽然不被认为是稳定的,但到目前为止它对我来说效果很好。 @T.J.Crowder:这是第一个有这个标志的版本。它的更新日志项是 V8 的升级【参考方案3】:

6.2.0 - 带有“use strict”和“--harmony_tailcalls”

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

带有“use strict”和“--harmony”的7.2.0

即使有 99999999999999 次调用,flag 也能无缝快速地工作。

【讨论】:

99999999999999?调用次数太多了。它会以某种方式优化计算吗?【参考方案4】:

更简洁的答案...截至实施之日,如前所述...

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。

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

【讨论】:

是的。只是为了让 CPU 烧毁,将它额外增加了几个数量级。

以上是关于Node.js 尾调用优化:可能与否?的主要内容,如果未能解决你的问题,请参考以下文章

服务器可以检查请求是不是来自 iframe (Node.js)?

node.js操作数据库之MongoDB+mongoose篇

为啥代码会主动尝试阻止尾调用优化?

关于ES6尾调用优化

关于ES6尾调用优化

带你从零学Node.js