Короткие полезные уроки JavaScript - сделайте это легко.

Перед применением любой оптимизации вы должны понять, работает ли ваш код на критическом пути. Если это не критический путь, скорее всего, ваши оптимизации не имеют большого значения, а преждевременная оптимизация в целом - плохая идея.

Итак, что такое преждевременная оптимизация? Это предложение взято из известной цитаты Дональда Кнута: «Преждевременная оптимизация - корень всех зол». Многие разработчики цитируют эту цитату, чтобы предположить, что большинство оптимизаций являются «преждевременными» и, следовательно, пустой тратой усилий. Правда более тонкая.

Если ваш код находится на критическом пути, эта оптимизация вас заинтересует, в противном случае лучше выделить время, чтобы сделать код более читабельным.

Что такое оптимизация совокупной стоимости владения?

Оптимизация хвостового вызова связана с определенным типом оптимизации, которая может происходить с вызовами функций. По словам Кайла Симпсона, хвостовой вызов - это вызов функции, который появляется в хвосте другой функции, так что после завершения вызова больше нечего делать.

Оптимизация хвостового кода берет рекурсивную функцию и генерирует итеративную функцию, используя «goto» внутри, а затем выполняет ее. Он не ограничивает вызовы стека, потому что их нет, и функция больше не является рекурсивной функцией. Производительность этой итерационной функции эквивалентна ее рекурсивной функции.

Другими словами, оптимизация хвостового вызова означает, что можно вызывать функцию из другой функции без увеличения стека вызовов.

Например, здесь у нас есть хвостовой вызов:

function doA(a) {
    return a;
}
function doB(b) {
    return doA( b + 1 ); //tail call
}
function foo() {
    return 20 + doB(10); //not tail call
}
foo(); // 31

doA (b + 1) - это хвостовой вызов в doB (b). После завершения doA (b + 1) doB (..) также завершается, и ему нужно только вернуть результат вызова doA (b + 1). Однако doB (10) не является хвостовым вызовом, потому что после его завершения его результат должен быть добавлен к 20, прежде чем foo () сможет его вернуть.

Для вызова новой функции требуется дополнительный объем зарезервированной памяти для управления кадром стека. Таким образом, предыдущий фрагмент обычно требует кадра стека для каждого вызова.

Однако, если механизм с поддержкой TCO может понять, что вызов doA (b + 1) находится в хвостовой позиции, что означает, что doB (b) в основном завершен, то при вызове doA (b + 1) ему не нужно создавать новый кадр стека, но вместо этого можно повторно использовать существующий кадр стека из doB (b). Это быстрее и требует меньше памяти.

Такая оптимизация становится критически важной при работе с рекурсией, особенно если рекурсия могла привести к тысячам кадров стека. С TCO механизм может выполнять все эти вызовы в одном кадре стека.

Рассмотрим эту рекурсивную функцию без оптимизации совокупной стоимости владения

Эта функция не поддается оптимизации, поскольку последняя инструкция - это не вызов другой функции, а выражение, которое умножает n на значение, возвращаемое факториалом (n-1). Это не позволяет механизму удалить стек, заставляя его создавать новый, в котором хранятся начальные значения.

function factorial(n) {
    console.trace();
    
    if (n === 0) {
        return 1;
    }
    // no proper tail call
    return n * factorial(n - 1);
}
factorial(2);
VM31:4 console.trace
factorial @ VM31:4
VM31:4 console.trace
factorial @ VM31:4
factorial @ VM31:10
VM31:4 console.trace
factorial @ VM31:4
factorial @ VM31:10
factorial @ VM31:10

Чтобы сделать предыдущую функцию оптимизированной движком, нам просто нужно удалить выражение n * factorial (n-1) и изменить функцию, чтобы она делала то же самое, но по-другому.

'use strict'; 
function factorial(n, total = 1) {
    console.trace();
    if (n === 0) {
        return total;
    }
    // proper tail call
    return factorial(n - 1, n * total);
}
factorial(2);
Trace
    at factorial
Trace
    at factorial
Trace
    at factorial

Здесь нет увеличения размера стека вызовов. Это означает, что таким образом вы не столкнетесь с ошибкой превышения максимального размера стека вызовов.

Последние заметки

Оптимизация хвостового вызова является частью спецификации ES2015-ES6. Его поддержка - это не NodeJS, это то, что должен поддерживать движок V8, который использует NodeJS. Узлы с 7.10 до 6.5.0 поддерживают это только в строгом режиме и с флагом «--harmony».

О неопределенном будущем TCO см. Этот ответ в stackoverflow: Оптимизация хвостовой рекурсии ES6.

Если это было полезно для вас, нажмите кнопку хлопка ниже. Большое спасибо!