Оптимизация хвостового вызова 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);
Я немного покопался и нашел это. Здесь, кажется, говорят, что я должен написать это так:
function* foo(x) {
if (x == 1) {
return 1;
}
else {
yield foo(x-1);
}
}
foo(100000);
Тем не менее, это дает мне синтаксические ошибки. Я пробовал разные варианты, но во всех случаях Node.js чем-то недоволен.
По сути, я хотел бы знать следующее:
- Node.js делает TCO или нет?
- Как это волшебно
yield
вещь работает в Node.js?
4 ответа
Здесь есть два довольно четких вопроса:
- Node.js делает TCO или нет?
- Как работает эта волшебная вещь в Node.js?
Node.js делает TCO или нет?
TL; DR: больше нет, начиная с Node 8.x. Некоторое время это делалось под тем или иным флагом, но на момент написания этой статьи (ноябрь 2017 года) этого больше не происходило, потому что используемый им движок V8 JavaScript больше не поддерживает TCO. Смотрите этот ответ для получения дополнительной информации.
Подробности:
Оптимизация Tail-Call (TCO) является обязательной частью спецификации ES2015 ("ES6"). Таким образом, его поддержка, прямо, не вещь NodeJS, это то, что движок JavaScript V8, который должен использовать NodeJS, должен поддерживать.
Начиная с Node 8.x, V8 не поддерживает TCO, даже за флагом. Это может сделать (снова) в какой-то момент в будущем; см. этот ответ для получения дополнительной информации.
Узел с 7.10 по крайней мере до 6.5.0 (мои заметки говорят о 6.2, но http://node.green/ не согласен) поддерживал TCO за флагом (--harmony
в 6.6.0 и выше, --harmony_tailcalls
ранее) только в строгом режиме.
Если вы хотите проверить свою установку, вот тесты, которые использует http://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 правда правда
Как это волшебно
yield
вещь работает в Node.js?
Это еще одна вещь ES2015 ("функции генератора"), так что опять же это то, что V8 должен реализовать. Он полностью реализован в версии V8 в Node 6.6.0 (и был для нескольких версий) и не стоит за флагами.
Функции генератора (написанные с function*
и используя yield
) работать, имея возможность останавливать и возвращать итератор, который фиксирует их состояние и может использоваться для продолжения их состояния в последующем случае. У Алекса Раушмейера есть углубленная статья о них здесь.
Вот пример использования итератора, возвращаемого функцией генератора в явном виде, но обычно вы этого не сделаете, и мы вскоре увидим, почему:
"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
Вот как это работает:
- Когда мы звоним
counter
(let it = counter(0, 5);
), начальное внутреннее состояние вызоваcounter
инициализируется и мы сразу получаем итератор; ни один из фактического кода вcounter
работает (пока). - призвание
it.next()
запускает код вcounter
до первогоyield
заявление. В таком случае,counter
делает паузу и сохраняет свое внутреннее состояние.it.next()
возвращает объект состояния сdone
флаг иvalue
, Еслиdone
флагfalse
,value
это значение, полученное отyield
заявление. - Каждый звонок
it.next()
продвигает состояние внутриcounter
к следующемуyield
, - Когда звонок в
it.next()
маркиcounter
Finish и Return, объект состояния, который мы получаем, имеетdone
установлен вtrue
а такжеvalue
установить возвращаемое значение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
проверяет нас.
node.js наконец-то поддерживает TCO с 2016.05.17, версия 6.2.0.
Это должно быть выполнено с --use-strict --harmony-tailcalls
флаги для ТШО для работы.
6.2.0 - с использованием "строгого" и "--harmony_tailcalls"
работает только с небольшими хвостовыми оптимизированными рекурсиями в 10000 (как в вопросе), но завершается неудачно, функция вызывает себя 99999999999999 раз.
7.2.0 с "используйте строгий" и "--harmony"
флаг работает легко и быстро даже при 99999999999999 звонках.
Более краткий ответ... по состоянию на дату реализации, как уже упоминалось...
ТШО работает. Это не пуленепробиваемый, но он очень приличный. Вот факториал (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, хотя.
Что касается урожайности, см. Другие ответы. Наверное, должен быть отдельный вопрос.