- PVSM.RU - https://www.pvsm.ru -
В погоне за производительностью разработчики изобретают самые разные способы оптимизации программ. В нашем случае речь идёт о повышении скорости работы функций. Пожалуй, в JavaScript их по праву можно назвать одним из краеугольных камней языка. В частности, функции — это средство разбиения программ на модули и инструмент для повторного использования кода.
Некоторые функции выполняются так быстро, что их многократный вызов, хотя и создаёт нагрузку на систему, проблемой не является. Некоторые же весьма «тяжелы», каждое обращение к таким функциям ведёт к серьёзным затратам вычислительных ресурсов. Если траты оправданы, вычисления оптимизированы, то деваться особо некуда. Но как быть, если при повторных вызовах, функция иногда (или, возможно, довольно часто) выполняет те же самые вычисления, которые выполнялись при её предыдущих вызовах? Можно ли этим воспользоваться для повышения производительности?
Функция вычисления факториала [2] — это пример ресурсоёмкой функции, которая, практически гарантированно, в ходе нескольких вызовов, выполняет некоторую часть одинаковых вычислений по много раз. Это открывает возможности по оптимизации через кэширование.
Например, вот функция factorial
, которая вычисляет и возвращает факториал числа. Если не вдаваться в детали её реализации, выглядеть она будет так:
function factorial(n) {
// Вычисления: n * (n-1) * (n-2) * ... (2) * (1)
return factorial
}
Вызовем её следующим образом: factorial(50)
. Она, как и ожидается, найдёт и возвратит факториал числа 50. Всё это хорошо, но давайте теперь найдём с её помощью факториал числа 51. Компьютер снова выполнит вычисления, и то, что нам надо, будет найдено. Однако, можно заметить, что, при повторном вызове, функция выполняет массу вычислений, которые уже были выполнены ранее. Попытаемся функцию оптимизировать. Подумаем, как, имея значение factorial(50)
перейти к factorial(51)
без повторного вызова функции. Если следовать формуле вычисления факториала, окажется, что factorial(51)
это то же самое, что и factorial(50) * 51
.
При подобном подходе, однако, выигрыша в производительности получить не удастся. А именно, сначала, внутри функции factorial()
производится полная цепочка вычислений для нахождения факториала 50, а потом то, что получилось, умножается на 51. То есть, при использовании подобной функции, вычисление факториала для числа 51 в любом случае выглядит как перемножение всех чисел от 1 до 51.
Хорошо было бы, если бы функция factorial()
умела запоминать результаты вычислений, выполненных при её предыдущих вызовах и использовать их при следующих вызовах для ускорения производительности.
Задаваясь вопросом о сохранении результатов предыдущих вызовов функции, мы приходим к идее мемоизации. Это методика, которой функции могут пользоваться для запоминания (или, другими словами, кэширования) результатов. Теперь, когда вы, в общих чертах, понимаете, чего мы хотим достичь, вот более строгое определение мемоизации [3]:
Мемоизация — сохранение результатов выполнения функций для предотвращения повторных вычислений. Это один из способов оптимизации, применяемый для увеличения скорости выполнения компьютерных программ.
Проще говоря, мемоизация — это запоминание, сохранение чего-либо в памяти. Функции, в которых используется мемоизация, обычно работают быстрее, так как при их повторных вызовах с одними и теми же параметрами, они, вместо выполнения неких вычислений, просто считывают результаты из кэша и возвращают их.
Вот как может выглядеть простая функция с мемоизацией. Этот код есть на CodePen [4], так что можете тут же с ним поэкспериментировать.
// простая функция, прибавляющая 10 к переданному ей числу
const add = (n) => (n + 10);
add(9);
// аналогичная функция с мемоизацией
const memoizedAdd = () => {
let cache = {};
return (n) => {
if (n in cache) {
console.log('Fetching from cache');
return cache[n];
}
else {
console.log('Calculating result');
let result = n + 10;
cache[n] = result;
return result;
}
}
}
// эту функцию возвратит memoizedAdd
const newAdd = memoizedAdd();
console.log(newAdd(9)); // вычислено
console.log(newAdd(9)); // взято из кэша
Проанализировав вышеприведённый фрагмент кода, можно сделать следующие выводы:
memoizeAdd
возвращает другую функцию, которую мы можем вызвать тогда, когда нужно. Такое возможно потому что функции в JavaScript — это объекты первого класса, что позволяет использовать их как функции высшего порядка [5] и возвращать из них другие функции.
cache
может хранить данные между вызовами функции, так как она определена в замыкании [6].
cache
ведёт себя именно так, как ожидается.
Вышеописанный код работает как надо, но что если нам хотелось бы превратить любую функцию в её вариант с мемоизацией. Вот как писать такие функции. Этот код, опять же, есть на CodePen [7].
// простая чистая функция, которая возвращает сумму аргумента и 10
const add = (n) => (n + 10);
console.log('Simple call', add(3));
// простая функция, принимающая другую функцию и
// возвращающая её же, но с мемоизацией
const memoize = (fn) => {
let cache = {};
return (...args) => {
let n = args[0]; // тут работаем с единственным аргументом
if (n in cache) {
console.log('Fetching from cache');
return cache[n];
}
else {
console.log('Calculating result');
let result = fn(n);
cache[n] = result;
return result;
}
}
}
// создание функции с мемоизацией из чистой функции 'add'
const memoizedAdd = memoize(add);
console.log(memoizedAdd(3)); // вычислено
console.log(memoizedAdd(3)); // взято из кэша
console.log(memoizedAdd(4)); // вычислено
console.log(memoizedAdd(4)); // взято из кэша
Отлично! Наша функция memoize
способна превращать другие функции в их эквиваленты с мемоизацией. Конечно, этот код не универсален, но его несложно переделать так, чтобы функция memoize
могла бы работать с функциями, имеющими любое количество аргументов.
Подобное можно написать самостоятельно, но существуют и библиотечные решения:
_.memoize(func, [resolver])
@memoize
из decko [10].
Если попытаться передать рекурсивную функцию рассмотренной выше функции memoize
, или функции _.memoize
из Lodash, то, что получится, будет работать неправильно, так как рекурсивные функции вызывают сами себя, а не то, что получается после добавления возможностей по мемоизации. Как результат, переменная cache
в такой ситуации не выполняет своего назначения. Для того, чтобы решить эту проблему, рекурсивная функция должна вызывать свой вариант с мемоизацией. Вот как можно добавить мемоизацию в рекурсивную функцию вычисления факториала [11]. Код, как обычно, можно найти на CodePen [12].
// уже знакомая нам функция memoize
const memoize = (fn) => {
let cache = {};
return (...args) => {
let n = args[0];
if (n in cache) {
console.log('Fetching from cache', n);
return cache[n];
}
else {
console.log('Calculating result', n);
let result = fn(n);
cache[n] = result;
return result;
}
}
}
const factorial = memoize(
(x) => {
if (x === 0) {
return 1;
}
else {
return x * factorial(x - 1);
}
}
);
console.log(factorial(5)); // вычислено
console.log(factorial(6)); // вычислено для 6, но для предыдущих значений взято из кэша
Проанализировав этот код, можно сделать следующие выводы:
factorial
рекурсивно вызывает свою версию с мемоизацией.
factorial(5)
.
Мемоизация — это разновидность кэширования. Обычно под кэшированием понимают довольно широкий набор способов сохранения чего-либо для последующего использования. Например, это может быть HTTP-кэширование. Мемоизация же обычно означает кэширование возвращаемых значений функций.
Хотя может показаться, что техника мемоизации настолько хороша, что может и должна стать частью всех функций, она, на самом деле, имеет ограниченное применение. Вот некоторые соображения, касающиеся использования мемоизации.
Уважаемые читатели! Если у вас есть примеры использования мемоизации в реальных проектах — поделитесь пожалуйста. Уверены, многим будет интересно о них узнать.
Автор: ru_vds
Источник [15]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/259576
Ссылки в тексте:
[1] Image: https://habrahabr.ru/company/ruvds/blog/332384/
[2] факториала: https://ru.wikipedia.org/wiki/%D0%A4%D0%B0%D0%BA%D1%82%D0%BE%D1%80%D0%B8%D0%B0%D0%BB
[3] мемоизации: https://ru.wikipedia.org/wiki/%D0%9C%D0%B5%D0%BC%D0%BE%D0%B8%D0%B7%D0%B0%D1%86%D0%B8%D1%8F
[4] CodePen: https://codepen.io/divyanshu013/pen/xdQPvp?editors=0011
[5] функции высшего порядка: http://eloquentjavascript.net/05_higher_order.html#h_xxCc98lOBK
[6] замыкании: https://developer.mozilla.org/en/docs/Web/JavaScript/Closures
[7] CodePen: https://codepen.io/divyanshu013/pen/zwMPdK?editors=0011#code-area
[8] Lodash: https://lodash.com/docs/4.17.4#memoize
[9] декораторами: https://babeljs.io/docs/plugins/transform-decorators/
[10] decko: https://github.com/developit/decko#memoize
[11] факториала: https://en.wikipedia.org/wiki/Factorial
[12] CodePen: https://codepen.io/divyanshu013/pen/JNevOm
[13] HTTP-кэш: https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching
[14] reselect: https://github.com/reactjs/reselect#creating-a-memoized-selector
[15] Источник: https://habrahabr.ru/post/332384/
Нажмите здесь для печати.