Это для цикла повторяется несколько раз? - PullRequest
0 голосов
/ 31 декабря 2018

Я обсуждал некоторый код с коллегами:

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.

Ответы [ 3 ]

0 голосов
/ 31 декабря 2018

Ваш первый пример (цикл for in) - 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может привести к тому, что выполнение перейдет к следующей итерации, оно все еще считается итерацией цикла .

0 голосов
/ 01 января 2019

Правильный ответ: «Это действительно не имеет значения».В некоторых ранее опубликованных ответах говорится, что второй подход - 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% медленнее.В этом суть этого ответа: не доверяйте деталям реализации каждого интерпретатора, потому что они могут (и будут) меняться со временем.Если вы не разрабатываете для встраиваемых систем или систем с высокой эффективностью / малой задержкой - где вам нужно быть как можно ближе к аппаратному обеспечению - сначала ознакомьтесь со сложностями алгоритма.

0 голосов
/ 31 декабря 2018

Простой способ узнать, сколько раз вызывается каждая часть этого оператора, - добавить операторы журнала, подобные приведенным, и запустить его в консоли 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

. Еще одно хорошее место для поиска - разработчик MDN.docs здесь особенно в разделе «Polyfill», в котором описан точный эквивалентный алгоритм, и вы можете видеть, что .filter() здесь возвращает переменную res, что и будет выполнено .forEach.

Таким образом, в то время как он выполняет итерацию по всему набору дважды, в разделе .forEach он выполняет итерацию только по той части набора, которая соответствует условию .filter.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...