[javascript:perf] Сравнение .reduce, .map, .filter, for
Введение
Зачастую, решение задачи, которое возможно написать кодом на языке Javascript, может быть различным. Критерии, оценивающие производительность кода, также зачастую могут различаться. Кто-то обращает внимание на читаемость, потому что ему важен порог входа и простота решения. А кто-то, в ущерб читаемости, уделяет производительности больше внимания.
Но в то же время, человек привыкает ко всему. И, если привыкнуть писать производительные решения на языке Javascript, то будет превалировать большее понимание того, что происходит на нижнем уровне, дабы то, что вы написали - работало.
Будем рассматривать задачи с применением массивов. Собственно, очень часто встает вопрос:
«Какие методы и как их использовать?»
Цель материала - ответить на этот вопрос. Таким образом, под раздачу у нас попадают задачи с использованием массивов, их методов и циклов. В этом материале будут изложены замеры на примерах, даны комментарии по каждому варианту, в каких случаях и что стоит использовать, а также будут даны конечные выводы и сводная таблица.
Предыстория
Методы .reduce, .map, .filter являются функциями высшего порядка. И, когда только их вводили в эксплуатацию, то они были своего рода обертками над циклом for. Этот факт, делал их медленнее чем традиционный for, поэтому за ними тянулась нехорошая практика и многие по-прежнему предпочитали for. Но, это длилось недолго, и на данный момент методы массивов .reduce, .map, .filter "разогнаны" так, что стоит использовать уже их, вместо for.
Да, старый добрый старичок for всем понятен и привычен. Поэтому впервые столкнувшись с решениями, которые усыпаны .reduce, можно столкнуться с проблемой читаемости и понимания всего того, что будет написано. Но, как было выше уже упомянуто - это вопрос времени, а точнее практики.
Итак, давайте сразу к коду. Начнем разбор.
Сравнения методов и циклов на задачах
Решено сразу в омут с головой прыгать, чтобы потом не собирать все воедино. После сравнения будет рассказано уже отдельно про каждый метод, а пока есть несколько сценариев, на которых будем проверять.
Сперва заведем функцию замера. Назовем ее benchmark, аргументами будет callback-функция и собственно количество итераций (iterations):
function benchmark(callback, iterations) {
const start = performance.now();
for (let i = 0; i < iterations; i++) {
callback();
}
const end = performance.now();
// Получим время выполнения (в мс и сек)
const duration = end - start;
const seconds = duration / 1000;
// Получим операций в секунду
const opSec = 1 / seconds
// Вывод в консоль замера
// Для красоты оставим с 3 знаками после запятой
console.log(
`Время выполнения: ${duration.toFixed(3)} мс; ` +
`Операций в секунду: ${opSec.toFixed(3)}`
);
}Сценарий: есть массив чисел, и мы хотим получить их сумму в одной переменной. Для чистоты экспериментов, будем использовать оператор var, и делать замеры на итерациях 10, 100, 1000, 10000, 100000.
Наш код с callback-функциями для benchmark:
const arr = [1, 2, 3, 4, 5];
// Вариант с методом reduce
const reduceBenchmark = () => {
var result = arr.reduce((acc, num) => acc + num, 0);
};
// Вариант с методом map
const mapBenchmark = () => {
var result = 0;
arr.map(num => result += num);
};
// Вариант с методом filter
const filterBenchmark = () => {
var result = 0;
arr.filter(num => {
result += num
});
};
// Вариант с циклом for
const forBenchmark = () => {
var result = 0;
for (var i = 0; i < arr.length; i++) {
result += arr[i];
}
};
const forBenchmarkOpt = () => {
var result = 0;
for (var i = arr.length - 1; i > 0; i--) {
result += arr[i];
}
};
// Вариант с циклом for...of
const forOfBenchmark = () => {
var result = 0;
for (const num of arr) {
result += num;
}
};Из практик по оптимизации для цикла for, лучше обратный проход, потому что достаточно только раз обратиться к arr.length. Поэтому приведен пример того как обычно будет использоваться for, и того, который даст выигрыш.
Дальше дело за малым, задаем количество итераций и запускаем наши benchmark'и:
const iterations = 1000000
console.log('Бенчмарк для метода reduce:');
benchmark(reduceBenchmark, iterations);
console.log('Бенчмарк для метода map:');
benchmark(mapBenchmark, iterations);
console.log('Бенчмарк для метода filter:');
benchmark(filterBenchmark, iterations);
console.log('Бенчмарк для цикла for:');
benchmark(forBenchmark, iterations);
console.log('Бенчмарк для цикла forOpt:');
benchmark(forBenchmarkOpt, iterations);
console.log('Бенчмарк для цикла for...of:');
benchmark(forOfBenchmark, iterations);Сделав по 3 замера на каждом числе итераций (10, 100, 1000, 10000, 100000) и, взяв среднее арифметическое по операциям в секунду, получаем следующий результат:
| 10 | 100 | 1000 | 10000 | 100000 | 1000000 | 10000000 | |
| Метод reduce | 16021,682 | 5437,705 | 610,082 | 238,504 | 50,865 | 7,839 | 0,755 |
| Метод map | 14243,667 | 5187,426 | 1115,528 | 156,768 | 40,596 | 6,731 | 0,616 |
| Метод filter | 15666,861 | 5164,289 | 940,434 | 173,978 | 58,308 | 7,44 | 0,728 |
| Цикл for | 16351,763 | 5080,381 | 1125,238 | 256,552 | 53,777 | 6,099 | 0,613 |
| Цикл forOpt | 14056,014 | 6299,519 | 1082,338 | 290,159 | 62,026 | 6,979 | 0,725 |
| Цикл for..of | 17092,165 | 7256,907 | 1089,661 | 213,697 | 69,504 | 8,687 | 0,851 |