web-dev-qa-db-ja.com

Node.js末尾呼び出しの最適化:可能かどうか?

私はこれまでJavaScriptが好きで、Node.jsがTCOを提供していると主張する this の理由もあり、Node.jsをエンジンとして使用することにしました。ただし、この(明らかに末尾呼び出しの)コードを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でどのように機能しますか?
25
Koz Ross

ここには2つのかなり明確な質問があります。

  • Node.jsはTCOを実行しますか、実行しませんか?
  • この魔法の降伏はNode.jsでどのように機能しますか?

Node.jsはTCOを実行しますか?

TL; DRNode 8.xしばらくの間、フラグの背後で実行されましたが、この記事の執筆時点(2017年11月)では、使用する基盤となるV8 JavaScriptエンジンがTCOをサポートしなくなったため、サポートされなくなりました。詳細については、 この回答 を参照してください。

詳細:

末尾呼び出しの最適化(TCO)は、ES2015( "ES6")仕様の必須 部分です 。したがって、それをサポートすることは、直接、NodeJSのものではなく、NodeJSが使用するV8JavaScriptエンジンがサポートする必要があるものです。

Node 8.xの時点で、V8はフラグの背後でもTCOをサポートしていません。将来のある時点で(再び)サポートする可能性があります。 これを参照してください。詳細については、answer を参照してください。

ノード7.10から少なくとも6.5.0まで(私のメモでは6.2と書かれていますが、 node.green は同意しません)、フラグの背後にあるTCOをサポートしていました(6.6.0以降では_--harmony_、 _--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ではありません。上記を参照
 $ node --harmony tco.js 
 true 
 true 

この魔法のyieldはNode.jsでどのように機能しますか?

これはES2015のもう1つの機能(「ジェネレーター関数」)であるため、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 [。

仕組みは次のとおりです。

  • 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が終了して戻ると、返される状態オブジェクトのdonetruevalueに設定されます。 counterの戻り値に設定します。

イテレータと状態オブジェクトの変数を用意し、it.next()を呼び出し、doneプロパティとvalueプロパティにアクセスすることは、(通常)何を妨げるかという定型的なものです。私たちがやろうとしているので、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チェックを実行します。

43
T.J. Crowder

node.jsは、2016.05.17以降、ついにTCOをサポートします バージョン6.2.

TCOが機能するには、--use-strict --harmony-tailcallsフラグを指定して実行する必要があります。

4
yairchu

6.2.0-「usestrict」および「--harmony_tailcalls」を使用

(質問のように)10000の小さなテール最適化再帰でのみ機能しますが、関数はそれ自体を999999999999999回呼び出すことに失敗します。

7.2.0「usestrict」および「--harmony」

フラグは、99999999999999の呼び出しでも、シームレスかつ迅速に機能します。

1
Mihey Mik