Pull to refresh

Comments 51

array
  .filter(n => n % 2)
  .map(n => Math.sqrt(n))
  .slice(0, 5);

Но если нам надо "вычислить корень из первых пяти элементов", то зачем вообще такое писать?

array
  .filter(n => n % 2)
	.slice(0, 5)
  .map(n => Math.sqrt(n));

И все, не будет у вас разницы с лодашем и не нужно его тянуть.

Разница всё равно будет - filter обработает весь исходный массив, и создаст массив-результат, в котором всё равно будет больше элементов, чем нужно. То есть останется сложность O(N) и избыточное потребление памяти.

Проблема именно здесь

array
  .filter(n => n % 2)

Если элементов 10000 то filter пройдется по всему массиву все равно. А lodash сделает это более эффективно и при первых пяти элементах которые пройдут фильтр перейдет к map. Если я правильно понял.

Да, верно, я думал внимание в статье обращено именно к map().

Впрочем, все равно нет особого смысла подключать внешнюю библиотеку, если уж мы экономим на таких вещах. А то получается экономия путем подключения оверхеда.

const result = [];

for (let i=0; i < array.length; i++) {
	if (array[n] % 2) {
    result.push(Math.sqrt(array[n]));
		if (result.length === 5) {
      break;
    }
  }
}

Всё так. Если Lodash на клиенте приводит к слишком большому оверхеду, но код нужно ускорить - я сделаю так же. Если Lodash уже есть - можно написать на нём.

Здесь для меня главное было обратить внимание на саму концепцию - мы можем писать одновременно достаточно эффективный и легко читаемый и сопровождаемый код.

Lodash ускорит написание кода разработчиком, но тоже даст оверхэд в виде создания массива выполняемых операций, инициализации скоупа при вызове функций. Сложность остаётся O(N) (хотя тут тоже можно уйти в дебри того что находится под капотом у push и что sqrt хорошо бы подтянуть чуть ближе чтоб не искать Math в global), но простой цикл остаётся всегда быстрее. На малых объёмах я тоже ленюсь.

lodash выполнит свой код

_(array).filter(n => n % 2).map(n => Math.sqrt(n)).take(5).value();

примерно так:

  1. Выполняем filter, пока не нашлось подходящего элемента или не закончился входной массив. Если закончился входной массив - goto 5.

  2. Для найденного элемента вычисляем корень

  3. Запоминаем результат в выходном массиве

  4. В выходном массиве набралось 5 элементов? Если нет - goto 1.

  5. Возвращаем выходной массив.

Кстати, в случае использования на фронте типа яндекса можно пойти чуть дальше и соорудить препроцессор который будет подобные конструкции раскладывать в циклы с ифами и тем самым убирать накладные расходы на всю внутреннюю логику лодаша и создания\чистки контекстов в процессе. А если ещё и дифф алгоритм реакта уберёте, то можно будет на конференциях рассказывать о вкладе компании в борьбу с CO<sub>2</sub>

array.reduce((acc, value) => {
  acc.push(someFn(value));
  return acc;
}, [])

А это разве не array.map(someFn) ?

Правильно, в таком виде это вообще можно заменить на map. В оригинале в reduce было ещё немного логики. Я не придумал, как сделать, чтобы она не отвлекала от сути вопроса и выбросил её, прошу прощения.

Мне не нравится функция reduce. Она делает код нечитаемым. Надо вдумываться, мысленно представлять, как перемещаются данные, чтобы понять, что хотел сказать автор кода. Обычный цикл в разы проще прочесть. Когда видишь reduce, то складывается ощущение, что автор хотел не писать понятный код, а посоревноваться в знании экзотических конструкций.


Вот пример нечитаемого кода из статьи, в который надо долго вглядываться, чтобы понять, как он выполняется:


arr.reduce((count, x) => predicate(x) ? count + 1 : count, 0)

И вот, как его надо переписать:


countWhere(arr, predicate);

Либо:


count = 0;
for (x of arr) {
    if (predicate(x)) {
        count ++;
    }
}

Этот вариант занимает больше строк, но глаз сразу цепляется за for/if и видит суть кода. Тот, кто написал исходный код, хотел показать, какой он умный и как он умеет пользоваться функциональным подходом, но фактически он показал, что он глупый, так как не додумался вынести код в функцию с понятным названием countWhere.


Давайте не забывать, что функции вроде map/reduce пришли из функциональных языков, где нет нормальных циклов и приходится искать обходные пути. Это по сути костыли. В императивных языках циклы есть и надо ими пользоваться.


Цикл еще и быстрее, чем функции вроде map/forEach/reduce, так как нет накладных расходов на вызов функции.


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

React - это вообще отдельная тема, с ним действительно можно легко написать тормознутый UI. У меня о производительности кода на React есть отдельный рассказ https://habr.com/ru/company/yandex/blog/536682/.

А может быть, дело банально в привычке? Вы привыкли к циклам и условиям, но не привыкли к функциональщине, поэтому легко "выцепляете" шаблонный цикл-условие, но не выцепляете не менее шаблонный reduce. Да, на reduce можно нагородить нечитаемый код - как с помощью любого другого инструмента - но ваш пример явно не из этой категории, долго вглядываться там нужно только если reduce никогда не использовали.

И нет, map/reduce это не костыли, это инструменты, которые позволяют выразить мысль гораздо более явным образом, нежели циклы. Абстракция более высокого уровня.

Нет, не согласен. Во-первых, countWhere/countIf короче и читабельнее чем reduce, так как из названия понятно, что она делает. Во-вторых, если не использовать готовые функции, то цикл через for или forEach() будет читабельнее.

Готовые функции то да, но смысл огород городить ради одного раза. А вот насчёт читабельности - субъективно. Я не то чтобы очень много функциональщиной занимался, но определённый опыт есть, так что само слово reduce сходу говорит, что сейчас будет какая-то поэлементная обработка с агрегированием результата, а уж дальше выцепить шаблонное условие ничуть не сложнее обычного цикла. Если же говорить о какой-то более сложной обработке, то цепочка типа .map(...).filter(...).reduce(...) может оказаться в разы читабельней реализации через циклы и условия, т.к. каждый шаг чётко говорит, что именно он делает.

<сарказм>
for(var i = arr.length - 1, total = 0; i > -1; i--, total += predicate(arr[i]));

console.log(total);
</сарказм>

Для Boolean short circuit могу предложить ещё один вариант, если не хочется длинные вызовы в один объединять:

let result = true;
result = result && visibleAtPoint(left, bottom, element);
result = result && visibleAtPoint(right, bottom, element);
return result;

Ну послушайте. Этим "приемам" сто лет в обед. Это не эксклюзив от разработчика яндекса. Мало того, очень многих "приемов" связанных, например, битовыми манипуляциями нет и в помине. А ведь это очень эффективеые методы. Зачем выдавать накопленный общий опыт за свой? Это просто некрасиво.

Статья не об этом. Статья говорит "смотрите, мы работаем в Яндексе, у нас большая нагрузка, мы понимаем что железо стоит денег, электричество стоит денег и т. д.", Яндекс тут только из-за этого. Статья полезная, о таких вещах надо писать постоянно! Разжирение программного обеспечения это реальная проблема.

Нытьё: у меня наболело я только на днях разгребла кусок ужасающего кода проекта работы с WebRTC где буквально всё было на shared_ptr хотя крошечные менеджеры и ещё более крошеные механизмы достаточно было десяток раз создать на жирном стеке десктопной ОС, а главное никакой многопоточности или множественного владения объектами в коде просто нет и не должно быть. Для обмена сообщениями, а в проекте априори обмен всегда только лишь между парой потоков, скопипащены монструозные слоты-сигналы где на каждый слот, который ещё и подключается в runtime может подписываться толпа потоков и этот список там обрабатывается и внутри ещё всё обёрнуто прослойкой из copy on write и каждый мехенизм обёрнут своей синхронизацией -_- Корректный обратный вызов пока не сделан потому что надо архитектуру от адка оверинжиниринга во всём вычистить окончательно поскольку сейчас в 3 основных классах такая сложная связь через обсерверы, что получается хрень где не понятно кого кто должен создать чтобы корректно всё инициализировалось и кто кем владеет. Меня эта ситуация дико напрягает, потому что это высокопроизводительный проект, который должен потреблять как можно меньше ресурсов, где через WebRTC должна передаваться, с минимальными задержками куча информации, ещё и кодеки должны работать, да и в остальном машина будет нагружена полезным софтом помимо стриминга, а данные должны ходить в обе стороны с как можно минимальными задержками.

Мда, теперь на Хабре даже коммент отредактировать вообще нельзя :) Круто, повод ещё реже сюда ходить.
О, как мне это знакомо. Приходилось заниматься тем же самым. В WebRTC и Chrome в целом прямо видно нагромождение вековых отложений: плео-хром, мезо-хром, нео-хром. Все примитивы дублируют свой функционал на всех уровнях. Я не удивляюсь что webrtclib практически врос в chrome и использует кучу общих библиотек, без которых он не собирается и от которых его не отделить. Если смотреть на разные версии то видно как параллельно это все кусками рефакторится и выпиливается.
В самом начале меня поразило насколько это просто высокоуровневый кусок Хрома с классами которые просто один к одному лежат под Web API.
Потом еще пришлось самому реализовывать H.264 кодек, т.к. было необходимо аппаратное ускорение, а родной кодек является сторонней библиотекой, которая тащит за собой еще больший кусок Хрома.
О да, а мне из-за патентных маразмов придётся ещё либо ffmpeg либо ещё что-то тащить в проект ради HVEC (h265) ^^'
Под реализацией своего кодека я как раз и имел в виду ffmpeg. Но всё равно, пришлось реализовывать всю обвязку для chroma, что бы он мог использовать аппаратное ускорение, менять битрейт, частоту кадров, передавать медиа-семплы без копирования и т.д.

Если статья "не об этом" зачем такой заголовок? "Однажды мне в голову пришла идея обобщить свой опыт и систематизировать приёмы ускорения работы кода" это тоже "не об этом"?

Все эти методы должны входить в обязательный курс разработчика.

Да, должны входить в обязательный курс :) Но мы живём в реальном мире, кто-то может давно начинал, кто-то просто не придавал этому значению, поэтому писать о таком надо. Чтобы веб работал хорошо, быстро и у пользователя был положительный опыт.

"просто не придавал этому значению“.. Это вообще законно?

Большое спасибо за статью, но позвольте небольшой оффтопчик? Ускорение JS кода это, конечно, здорово, но может ли Яндекс исправить, наконец, утечки памяти на Дзене? У меня некоторые статьи там убивают вкладку в хроме ещё на этапе загрузки страницы.

У меня еще веб-страница Яндекс Радио стала дико течь. Говорят из-за рекламы, фиг знает.

Ноутбук начинает шуметь так, как будто обрабатывает что-то.

Перешел на Spotify из-за этого

Мяф, я не работаю в Яндекс, но, буквально за пару кликов нашла вот это:

yandex.ru/support/zen/troubleshooting/feedback.html

То что Яндекс сфейлился и у них нет технического фидбека наружу ¯\_(ツ)_/¯ но жаловаться надо точно куда-то туда. Тут точно не услышат.

P. S. и для Forum3 тоже ^^

в статье много раз употребляется термин "горячий код". А что это значит?

Чаще всего в приложении разные части кода выполняются разное количество раз (например, часть кода выполняется только один раз при старте, часть - один раз на запрос или действие пользователя, часть выполняется O(N) раз, часть - O(N^2) раз и так далее). Код, который выполняется чаще всего, многие называют "горячим".

Исторически это может быть связано с компилятором HotSpot для виртуальной машины Java - в нём есть понятие "hot spots", горячих точек в коде, которые нужно компилировать в первую очередь. В JProfiler, инструменте анализа производительности для Java, один из режимов просмотра так и назвали "Hot Spots".

Дополню, что, например, в Firefox в консоли по F12 есть профайлер, в других браузерах это тоже есть, там можно посмотреть топовые места по нагрузке, но дальше надо понять кто эту нагрузку спровоцировал, например большое количество дёрганий DOM или что-то подобное ;)

Поиск максимального элемента в массиве:

Math.max(...array)

Только для не слишком большого массива, ибо берегите стек.

Посчитал на устройствах, которые были под рукой, максимальный размер массива чисел, который можно таким образом передавать https://jsfiddle.net/obqpag3c/

Chrome 92 Windows - 125670

Chrome 92 Android 9 - 220956

Safari 13.1.3 macOS - 65536

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

Firefox 90 Windows - 500000
Vivaldi 4 Android 10 - 110437
Интересно было бы по скорости сравнить с _.max(array). С одной стороны спрэд увеличивает время вызова, с другой - встроенная функция, которая должна работать "вжжжух". Может, как-нибудь сподоблюсь сравнить.
Чаще всего размер массива известен и невелик, поэтому способ имеет право пожить.

На вот таком простом бенчмарке https://jsbench.me/25ksad2y1a/1

  • в Chrome 92 на macOS Math.max(...array) быстрее примерно в 1.5-2 раза на всех допустимых для него размерах массива

  • в Chrome 92 на Android 9 - аналогично

  • в Safari 13.1 на macOS наоборот, Math.max(...array) медленнее примерно в 1.5-2 раза

Такими методами, досрочно завершающими перебор, у массива являются find, findIndex, every и some. Есть проблема с методом reduce: он продолжает перебор массива до конца.

Можно выбросить исключение.

Если выбросить исключение - reduce вернёт undefined. То есть, чтобы всё-таки получить результат вычислений, надо будет вместо аккумулятора использовать внешнюю переменную. Дальше надо будет завернуть reduce в try-catch, чтобы отловить наше исключение. И ещё надо будет не съесть в try-catch все остальные исключения. В итоге у меня получилось примерно так:

const BREAK_ERROR = 'BREAK'; // наше исключение для выхода из reduce
let result = ...; // аккумулятор
try {
  array.reduce(function(_, n) {
    result = ...; // накапливаем значения в аккумуляторе
    if (/* условие прерывания reduce */) {
      throw new Error(BREAK_ERROR);
    }
  }, undefined); // стандартный аккумулятор не используем
} catch (e) {
  if (e.message === BREAK_ERROR) {
    // прерывание reduce - игнорируем
  } else {
    // все остальные ошибки бросаем дальше
    result = undefined;
    throw e;
  }
}

Конечно, это тоже можно завернуть в функцию а-ля _.transform и убрать с глаз долой. Но кроме некрасивого кода получаем ещё и разницу в скорости работы - на небольших массивах такой код заметно медленнее, чем _.transform https://jsbench.me/lbksaaah54/1

При выходе из reduce/transform на середине массива у меня получились такие результаты:

  • массив из 100 элементов - transform на порядок быстрее

  • 1000 элементов - transform примерно в два раза быстрее

  • 10000 элементов - transform примерно на 20% быстрее

  • 50000 элементов - скорости примерно одинаковы

  • 100000 элементов - reduce примерно на 20% быстрее

  • 500000 элементов - reduce примерно на 25% быстрее

  • дальше разница в скорости почти не меняется, остаётся порядка 25%

с reduce да, не очень. Я просто помню так foreach прерывал когда-то.

Предположим, вам необходимо сделать вычисление над частью массива:
array.slice(1).forEach()
Тогда, опять же, не нужны сложные цепочки вычислений — тоже можно использовать for, задав в нём нужный диапазон индексов:
for(let i = 1; i < len; i++)
Вы хотите сказать, что вот это:
array.slice(1).forEach(element => {
  // some actions
})
Это «сложные цепочки вычислений», а вот это:
const len = array.length;

for(let i = 1; i < len; i++) {
  const element = array[i];
  // some actions
}
↑ просто, коротко, не делает 5 вспомогательных операций и не вводит 2 служебных переменные?

Несмотря на то, что вариант с for читается хуже, он на самом деле обязан быть быстрее. Две служебные переменные вводит и slice и forEach, просто под капотом. Но slice создаёт новый лишний массив. Плюс настоящий forEach, внутри проверяет массив на дырки, использует .apply, и делает дополнительные type-проверки.


P.S. это не отменяет того что в 99% случаев вариант с .slice().forEach подходит бизнесу лучше, т.к. такой код легче писать\читать\поддерживать.

Я не спорю со скоростью (слов «быстрее»/«медленнее» я не случайно не использовал), мне просто непонятно, про какие «сложные цепочки вычислений» идёт речь. Причём, чтобы этих вычислений не было во втором варианте.

Я имел в виду сами цепочки вызовов array.slice(1).forEach(), array.slice(1).map() и т.п., а не код, который будет внутри forEach. Возможно, формулировка неудачная. Если подскажете лучший вариант формулировки - буду благодарен.

Крайне противоречивое впечатление о материале. Создается впечатление, что это на скору руку собранный документ причем в разное время и разными людьми. Иногда описано то, что хочется перепроверить, а иногда откровенные фантазии человека который не дал себе труда докопаться до настоящих причин приводящих к тому или иному результату.

Судите сами:

Мутабельность

Первая проблема — плохая поддержка иммутабельности. Иммутабельность объектов означает генерацию новых объектов, иногда с довольно большой скоростью [...] убрать клонирование объектов и мутировать существующие объекты.

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

Первое чему учат людей, которые хотят писать высокопроизводительный код для JS это мономорфность функций и hidden class для обьектов. Благодаря которым V8 (и не только он) может применять эффективные методы оптимизации кода работы с обьектами. Как первое так и второе априори ставит под вопрос вменяемость совета о мутировании обьектов. Если Ваши обьекты мутируют, то вы не только проигрываете из-за того что не получаете эффективные оптимизации от v8, но и можете еще больше замедлить свой код в случаях, когда V8 примете решение о применении своих оптимизаций, а потом будет вынуждена проводить деоптимизации после ваших мутаций.

Zero memory allocation или GC-free

это безусловно очень правильное поведение при условии соблюдения того о чем говорится выше. То есть нужно не только использовать детерменированный пул обьектов, но пул обьектов с одинаковыми hidden классами. В противном случае Вы сократив издержки работы GC получите еще большие издержки в связи с тем, что к вашему коду откажутся применять хоть сколько нибудь эффективные оптимизации.

Чтобы подчеркнуть важность этого: разница в производительности при работе оптимизированного кода и кода который принято решение не оптимизировать, может составлять и 10 и 100 и более раз.

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

map reduce filter etc...

Методы перечисленный выше не зря называют функциональными. Эти методы существую для реализации парадигмы функционального программирования, но не для бездумного применения map в качестве итератора по массиву. Ваша проблема, и проблема многих других, в том что вы и они понятия не имеют зачем map возвращает новый массив, для чего придуман reduce, и почему for никогда не будет быстрее forEach и что самое главное будет ему проигрывать в некоторых случаях в десятки раз.

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

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

Антипаттерн: накопление строк в массиве

Ещё один антипаттерн со времён шестого Internet Explorer — если нужно накопить длинную строку из кусочков, некоторые разработчики до сих пор сначала собирают эти строки в массив, чтобы потом вызвать join

это работало и работает по сей день и не только в IE. Большая проблема хипстеров пишущих микробенчмарки сравнивая тот или иной код в том, что они не понимают что сравнивают. Если я попрошу Вас сейчас набросать мне концепт такого теста, вы наверняка вляпаетесь в ровно теже проблемы что и все прочие, когда делали выводы либо о работе этого метода либо о противоположном результате. Я так категоричен потому, что ваша рекомендация о ConsString и рассказ легенды о IE прямо указывает на то, что Вы как им многие другие не понимали почему холивары на тему concat и + не утихали долгое время.

А проблема всегда была в том, что в большинстве случаев сравнивалось не время конкатенации, а время выделения ресурсов для обслуживания массива, и ресурсов для обслуживания строки состоящей из чанков. И как только тот же тест переписывается с исключением этой проблемы ( как самый простой способ - массив накапливающий строки должен иметь строго детерменированную длину равную количеству чанков) то результат внезапно становится совершенно другим. Когда вдруг array.concat оказывается быстрее ConsString

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

Idle Until Urgent

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

Предсказание ветвлений (Branch prediction)

Совершенно верно то, что на уровне процессора существует масса оптимизаций кода в том числе и ветвлений. Только позвольте вам задать вопрос - вы вообще в курсе каким образом работает V8?

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

Вы отдаете себе отчет в том, что только в том случае если Ваш код попал в TurboFan вы можете предсказать его конкретную реализацию под конкретную архитектуру?

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

И теперь для того чтобы ситуацию сделать еще более драматичной, для текущей x86 архитектуры, описание алгоритма оптимизации ветвлений на уровне процессора занимает почти 11 страниц 10 шрифтом. То есть, это оптимизации такого рода, которые ну совсем не для того кто пишет JS код. Но для того кто пишет оптимизирующие алгоритмы TurboFan.

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

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

Даунгрейд кода: ES6 → ES5

Ноги таких рекомендация растут из ситуации которую создали программисты V8 в то время, когда реализация многих новых методов делалась как простой полифил написанный на JS. То есть это был ровно такой же JS код, как если бы он был написан самостоятельно, с той лишь разницей что он проходил все тесты. То есть полностью соответствовал спецификации с учетом всех пограничных случаев. Стоит ли говорить, что когда Вы написали свой код упрощая его до нельзя он работал быстрее?

В настоящий момент, насколько мне не изменяет память, в ES6 не осталось ни одной подобной функции.

И тут я должен высказать большое ФЭ в сторону процесса разработки V8. Ситуация подобная с деструктурирующим присваиванием, или теми же map reduce и прочими методами, сильно подорвали веру в то, что Вы как разработчики V8 всегда делаете все хотя бы на троечку. Отчего все ваши последющие увещевания, не использовать специфические оптимизации можно спустить куда подальше, именно потому, что не Вам решать как должен работать мой код, тем более в ситуациях когда вы скармливаете людям откровенную халтуру. Пусть и исправляемую в последствии. Потому как именно Вы являетесь причиной того, что даются советы о даунгреде es5 с целью увеличения производительности в 2021 году.

Игого

Из всей статьи, в некотором смысле можно оставить только часть касающуюся работы с алгоритмами. Все прочие рекомендации представляют из себя либо действительно работавшие на момент релиза хаки (как в случае с даунгрейдом) либо невесть откуда взявшиtся рекомендации напрямую противоречащие тому, как работает V8 даже 10 летней давности. (V8 взят за основу как подавляюще доминирующая платформа выполнения JS. Многие из вещей заявленных мной выше характерны и для CoreJs и для SpiderMonkey)

Рекомендации

Самой верной рекомендацией на текущий момент для людей которые не любят профилировать свой код, или не хотят разбираться в кишках V8 это писать код который был бы максимально простым и предсказуемым, следуя простым правилам: никогда не меняйте структуру обьектов. Пишите код так как будто он у вас типизирован. Иными словами создав ссылку на область данных (переменную) где лежит число, сделайте так чтобы там никогда ничего кроме таких же числе не появлялось.

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

Если есть что-то, что может сделать за вас платформа - пусть это делает она. Например не стоит городить циклы for там, где с этим прекрасно справиться forEach. Внимательно читайте документацию и пытайтесь представить что означает то или иное действие: применяя map к массиву вы создаете новый массив, что неизбежно ведет к издержкам на обслуживание этой структуры данных. То есть каждая операция чего то стоит, и нужно понимать чего.

И дайте себе труд посмотреть пару лекций на эту тему от людей которые понимают то о чем говорят. Например

Franziska Hinkelmann https://www.youtube.com/watch?v=p-iiEDtpy6I&list=WL&index=148

или Вячеслава Егорова https://www.youtube.com/watch?v=HPFARivHJRY

Добрый день, я попытался воспроизвести ваш пример из пункта Антипаттерн: накопление строк в массиве , где вы предлагаете задать длину массива и в этом случае код должен исполнится быстрее. Не могли бы вы показать пример ?

В моем случае конкатенация строк по прежнему исполнялась в разы быстрее.

Мутабельность

Давать рекомендации подобного характера в отрыве от платформы ... Потому делаем предположение что в данном случае речь идет о JS.

Не надо делать предположение, надо прочитать параграф перед пунктом "Мутабельность": "Эта группа оптимизаций подходит для языков, в которых есть garbage collection и автоматическое управление памятью: JavaScript, C#, Java и некоторых других".

В статье я указал область применимости данной оптимизации: "Иммутабельность объектов означает генерацию новых объектов, иногда с довольно большой скоростью. А старые объекты должны собираться через garbage collector. В горячем коде это может очень сильно влиять на скорость работы. Именно затраты на сборку мусора могут превышать затраты на работу вашего кода." То есть мы уже попрофилировали наш прод и видим, что упираемся в память и сборщик мусора. В такой ситуации оптимизация работы с памятью - первоочередная вещь, и я даю конкретный совет, на что обратить внимание (на иммутабельность) и как это можно исправить (мутировать существующий объект).

Далее. Когда мутирование существующего объекта может создавать новый hidden class и мешать мономорфизму? Когда оно создаёт в объекте новые поля (или удаляет существующие, но с удалением полей всё ещё хуже и это тема отдельного длинного разговора). Меняя значения уже существующего поля так, чтобы тип поля не менялся, мы не создаём нового hidden class. И наоборот, поддерживая иммутабельность объектов, легко наступить на полиморфизм.

Резюмируя замечания по пункту "Мутабельность":

  • рекомендация не в отрыве от платформы; наоборот, я обратил внимание, для каких платформ и когда именно данная рекомендация применима

  • рекомендация ущерба мономорфизму не наносит: если в коде создаётся новое поле, то новый hidden class надо создавать в любом случае, а если меняется существующее поле, то как минимум хуже не станет

Zero memory allocation или GC-free

Чтобы подчеркнуть важность этого: разница в производительности при работе оптимизированного кода и кода который принято решение не оптимизировать, может составлять и 10 и 100 и более раз.

Такие утверждения желательно сопровождать ссылками на бенчмарк.

Антипаттерн: накопление строк в массиве

И как только тот же тест переписывается с исключением этой проблемы ( как самый простой способ - массив накапливающий строки должен иметь строго детерменированную длину равную количеству чанков) то результат внезапно становится совершенно другим. Когда вдруг array.concat оказывается быстрее ConsString

  1. Я писал про join, а не concat.

  2. [grammar]Детерминированную[/grammar]

  3. Желательно всё-таки показать правильный бенчмарк. И заодно научить, как правильно писать этот же код в реальном проекте, ибо строго детерминированную длину массива я в коде с join видел крайне редко, а иногда длина массива в принципе неизвестна заранее.

Idle Until Urgent

Все верно с одним важны НО. Все таже мономорфность и все те же хидден классы. [...] созданный обьект не должен изменять своей структуры.

Ещё раз внимательно перечитал пример кода

constructor() {
  this.formatter = new IdleValue(() => new Intl.DateTimeFormat(…));
}
handleUserClick() {
  const formatter = this.formatter.getValue();
  this.clickTime = formatter.format(new Date());
}

Не вижу связи между ленивой инициализацией IdleValue и созданием hidden class. Да, здесь, как и в оригинальном коде без ленивой инициализации, не в конструкторе создаётся поле this.clickTime, но при чём здесь IdleValue? Можем переписать этот пример так:

constructor() {
  this.formatter = new IdleValue(
    () => new Intl.DateTimeFormat(…));
  this.clickTime = '';
}
handleUserClick() {
  const formatter = this.formatter.getValue();
  this.clickTime = formatter.format(new Date());
}

Покажите, как в таком коде использование IdleValue ломает мономорфизм?

Предсказание ветвлений (Branch prediction)

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

Всё правильно. Большая часть кода никогда не попадёт в компилятор. Но нас и не интересует абсолютно весь код. Мы не занимаемся преждевременной оптимизацией всей кодовой базы - нас интересуют только обнаруженные узкие места, и для них предположение "никогда не будет собрана в машинный код, то есть будет исполняться на уровне байткода" скорее всего неверно.

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

Переформулирую другими словами то, о чём я писал в статье:

  • когда есть конкретное место в коде, где мы обрабатываем большие массивы данных (картинки, звук и т.п.)

  • и когда профилирование показало нам, что в это месте явная проблема в скорости

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

  • и ускорение в некоторых случаях действительно наблюдается в продакшне

и почему for никогда не будет быстрее forEach и что самое главное будет ему проигрывать в некоторых случаях в десятки раз.

Возможно я вас неправильно понял, но какие оптимизации могут сделать foreach быстрее, чем for? Мы всё ещё про JS массивы? Речь идёт о каких-то вырожденных случаях с "дырками"? Или про нестандартный foreach с ранним выходом (как в lodash)?


Под for подразумевается for (let idx = 0, count = arr.length; idx < count; ++ idx) { const item = arr[idx]; ... }? Или что-то более монструозное?


P.S. Касательно мутаций — автор явно имел ввиду не мутацию схемы hidden-class-а, а мутацию значений конкретных полей.

Осталось написать babel transform который всё это сделает за нас и можно будет спасть спокойно. Не возьмётесь?)
P.S.
array.some(x => Boolean(x))
->
array.some(Boolean)
:)
Sign up to leave a comment.