Это для цикла повторяется несколько раз?
Я обсуждал некоторый код с коллегами:
for(const a of arr) {
if(a.thing)
continue;
// do a thing
}
Было предложено отфильтровать это и использовать forEach.
arr.filter(a => !a.thing)
.forEach(a => /* do a thing */);
Была дискуссия об итерации больше, чем необходимо. Я посмотрел это и не могу ничего найти. Я также пытался выяснить, как просмотреть оптимизированный вывод, но я тоже не знаю, как это сделать.
Я ожидаю, что filter
и forEach
превратятся в код, очень похожий на код for of
с оператором continue
, но я не знаю, как в этом убедиться.
Как я могу узнать? Единственное, что я до сих пор пробовал, это Google.
Ответы
Ответ 1
Ваш первый пример (цикл for) - это O (n), которое будет выполнено n раз (n - размер массива).
Ваш второй пример (фильтр forEach) - O (n + m), который будет выполняться n раз в фильтре (n - размер массива), а затем m раз (m - размер результирующего массива после фильтра. происходит).
Таким образом, первый пример быстрее. Однако в этом типе примера без чрезвычайно большого набора образцов разница, вероятно, измеряется в микросекундах или наносекундах.
Что касается оптимизации компиляции, то это, по сути, вся оптимизация доступа к памяти. Все основные интерпретаторы и механизмы будут анализировать проблемы в коде, касающиеся доступа к функциям, переменным и свойствам, например, как часто и как выглядит форма графа доступа; а затем, используя всю эту информацию, оптимизируйте их скрытую структуру, чтобы сделать ее более эффективной для доступа. По сути, никакой оптимизации не происходит, поскольку в коде не выполняется замена цикла или анализ процесса, так как он по большей части оптимизируется во время его выполнения (если определенная часть кода начинает занимать слишком много времени, возможно, его код оптимизирован),
При первом выполнении кода JavaScript V8 использует полный кодекс, который напрямую переводит проанализированный JavaScript в машинный код без каких-либо преобразований. Это позволяет ему очень быстро начать выполнение машинного кода. Обратите внимание, что V8 не использует промежуточное представление байт-кода таким образом, устраняя необходимость в интерпретаторе.
Когда ваш код выполняется в течение некоторого времени, поток профилировщика собрал достаточно данных, чтобы указать, какой метод следует оптимизировать.
Затем, оптимизация коленчатого вала начинается в другом потоке. Он переводит дерево абстрактного синтаксиса JavaScript в высокоуровневое статическое представление с единым назначением (SSA) под названием Hydrogen и пытается оптимизировать этот граф Hydrogen. Большинство оптимизаций делается на этом уровне.
- https://blog.sessionstack.com/how-javascript-works-inside-the-v8-engine-5-tips-on-how-to-write-optimized-code-ac089e62b12e
* continue
может привести к тому, что выполнение перейдет к следующей итерации, но все равно будет считаться итерацией цикла.
Ответ 2
Правильный ответ: "Это действительно не имеет значения". В некоторых ранее опубликованных ответах говорится, что второй подход - O (n + m), но я позволю себе не согласиться. Точно такие же "m" операции также будут выполняться при первом подходе. В худшем случае, даже если вы рассматриваете второй пакет операций как "m" (что на самом деле не имеет особого смысла - мы говорим о тех же n элементах, заданных в качестве входных данных - а не о том, как работает анализ сложности), в в худшем случае m == n, а сложность будет O (2n), что в конечном итоге равно O (n).
Чтобы ответить непосредственно на ваш вопрос, да, второй подход будет повторять коллекцию дважды, а первый - только один раз. Но это, вероятно, не будет иметь никакого значения для вас. В подобных случаях вы, вероятно, захотите улучшить читабельность по сравнению с эффективностью. Сколько предметов у вашей коллекции? 10? 100? Лучше писать код, который будет легче поддерживать с течением времени, чем стремиться к максимальной эффективности все время - потому что в большинстве случаев это просто не имеет никакого значения.
Более того, повторение более одного раза не означает, что ваш код работает медленнее. Это все о том, что внутри каждой циклы. Например:
for (const item of arr) {
// do A
// do B
}
Практически так же, как:
for (const item of arr) {
// do A
}
for (const item of arr) {
// do B
}
Сам цикл for не добавляет значительных накладных расходов на процессор. Хотя в любом случае вы, вероятно, захотите написать один цикл, если читаемость кода улучшается, когда вы делаете два цикла, продолжайте и делайте это.
Эффективность заключается в выборе правильного алгоритма
Если вам действительно нужно быть эффективным, вы не хотите перебирать всю коллекцию, даже один раз. Вам нужен более умный способ сделать это: либо разделяй и властвуй (O (log n)), либо используй хеш-карты (O (1)). Хэш-карта в день защищает от неэффективности :-)
Делай вещи только один раз
Теперь вернемся к вашему примеру, если я обнаружу, что повторяюсь снова и снова и выполняю одну и ту же операцию каждый раз, я бы просто запустил операцию фильтрации только в начале:
// during initialization
const things = [];
const notThings = [];
for (const item of arr) {
item.thing ? things.push(item) : notThings.push(item);
}
// now every time you need to iterate through the items...
for (const a of notThings) { // replaced arr with notThings
// if (a.thing) // <- no need to check this anymore
// continue;
// do a thing
}
И тогда вы можете свободно перебирать notThings
, зная, что ненужные элементы уже отфильтрованы. Имеет смысл?
Критика " for of
быстрее, чем вызов методов"
Некоторые люди любят утверждать, что for of
всегда будет быстрее, чем вызов forEach()
. Мы просто не можем этого сказать. Существует множество интерпретаторов Javascript, и для каждой из них существуют разные версии, каждая из которых имеет свои способы оптимизации. Чтобы доказать свою точку зрения, я смог заставить filter() + forEach()
работать быстрее, чем for of
в Node.js v10 на macOS Mojave:
const COLLECTION_SIZE = 10000;
const RUNS = 10000;
const collection = Array.from(Array(COLLECTION_SIZE), (e, i) => i);
function forOf() {
for (const item of collection) {
if (item % 2 === 0) {
continue;
}
// do something
}
}
function filterForEach() {
collection
.filter(item => item % 2 === 0)
.forEach(item => { /* do something */ });
}
const fns = [forOf, filterForEach];
function timed(fn) {
if (!fn.times) fn.times = [];
const i = fn.times.length;
fn.times[i] = process.hrtime.bigint();
fn();
fn.times[i] = process.hrtime.bigint() - fn.times[i];
}
for (let r = 0; r < RUNS; r++) {
for (const fn of fns) {
timed(fn);
}
}
for (const fn of fns) {
const times = fn.times;
times.sort((a, b) => a - b);
const median = times[Math.floor(times.length / 2)];
const name = fn.constructor.name;
console.info('${name}: ${median}');
}
Время (в наносекундах):
forOf: 81704
filterForEach: 32709
for of
был последовательно медленнее во всех тестах, я побежал, всегда около 50% медленнее. В этом суть этого ответа: не доверяйте деталям реализации каждого интерпретатора, потому что они могут (и будут) меняться со временем. Если вы не разрабатываете для встраиваемых систем или систем с высокой эффективностью/малой задержкой - где вам нужно быть как можно ближе к аппаратному обеспечению - сначала узнайте сложности алгоритмов.
Ответ 3
Простой способ узнать, сколько раз вызывается каждая часть этого оператора, состоит в том, чтобы добавить подобные операторы журнала и запустить их в консоли Chrome.
var arr = [1,2,3,4];
arr.filter(a => {console.log("hit1") ;return a%2 != 0;})
.forEach(a => {console.log("hit2")});
"Hit1" должен печатать на консоль 4 раза независимо от этого случая. Если бы это повторялось слишком много раз, мы бы увидели результат "hit2" 4 раза, но после выполнения этого кода он выводит только дважды. Таким образом, ваше предположение частично верно, что во второй раз он повторяется, он не повторяется по всему набору. Однако он выполняет итерацию по всему набору один раз в .filter
а затем .filter
по той части набора, которая снова соответствует условию в .filter
Другое хорошее место, чтобы посмотреть это в документации для разработчиков MDN здесь, особенно в разделе "Polyfill", который обрисовывает в общих чертах алгоритма точного эквивалента, и вы можете видеть, что .filter()
здесь возвращает переменный res
, которая является то, что .forEach
будет выполняться после.
Таким образом, несмотря на то, что он в целом повторяет набор дважды, в разделе .forEach
он выполняет только ту часть набора, которая соответствует условию .filter
.