Когда реальность намного больше, чем то, что ты видишь
Медленный код замедляет вас? Вы когда-нибудь задумывались, что приводит к медленному коду? Что ж, мы все время от времени сталкиваемся с этой ситуацией, и я попытался выяснить, что может замедлять работу. Вы можете столкнуться с тем, что не совсем понимаете, как это работает внутри ядра. Это одна из причин, почему вы не можете выяснить, почему он работает медленно. Думая о медленном коде, вы забываете, что медленный код — это код для выполнения дополнительной работы. Дополнительная работа над вашим решением — один из фундаментальных принципов оптимизации производительности. Следовательно, глубокое изучение корней вашей архитектуры поможет вам решить проблемы с производительностью и сделает вас лучшим техническим экспертом, который знает, как писать стабильный и быстрый код.
Алгоритмическое мышление — это способ найти решение путем точного определения необходимых шагов — ничего не происходит по волшебству.
Давайте представим, что код, который вы написали, — это верхушка айсберга, а фреймворки, библиотеки, методы SaaS API и другие компоненты, которые вы используете, — это его основная часть. Легко быстро проверить, не ошиблись ли вы, и попытаться улучшить свое решение, но если вы посмотрите на айсберг под водой, то легко понять, что этих шагов недостаточно, чтобы ваше приложение работало быстро и работало должным образом.
Эти вопросы тоже всплывают в вашей голове?
- Не сложно ли найти проблему, если это не одна строчка кода?
- Что, если суть проблемы заключается в двух строках кода?
- Что, если эти две строки кода находятся в двух разных функциях/файлах/проектах?
- Не сложно ли найти такие проблемы и что бы вы сделали с таким бардаком?
Оптимизация
Оптимизация — это непрерывный процесс. Напишите код, опубликуйте код, и сейчас самое время оглянуться назад и выявить скрытые недостатки. В данном случае перепроектирование — это проблема, которая может испортить любое приложение. Когда мы смотрим на наши собственные результаты, мы часто выявляем области, которые мы можем не только улучшить, но и применить на практике. Я всегда стараюсь следовать лучшим практикам и оставлять резервное время для работы над производительностью приложений на последних итерациях цикла разработки — тот же подход, который я бы рекомендовал вам.
Когда мы достигнем этапа, когда нам нужно найти раздел, связанный с проблемами производительности, мы должны сосредоточиться на следующих шагах:
- Найдите, где он медленный, он же обнаруживает узкое место
- Поймите, почему это медленно, или почему это узкое место
- Подумайте, что с этим делать, ака оптимизировать
Обнаружить
Первым шагом в оптимизации является использование профилирования, инструментов разработки Chrome, консольного времени или, например, в Salesforce Commerce Cloud. Вы также можете использовать Code Profiler, Pipeline Profiler и Quota Statuss для поиска конфиденциальных разделов.
Понять и рассмотреть
Второй шаг — понять, где и что именно тормозит. На последнем шаге потребуются некоторые базовые знания, чтобы устранить обнаруженную загвоздку и применить решение для сломанных участков. Эти базовые знания не являются глубокими; это простой алгоритмический способ мышления, который может ускорить и облегчить нашу жизнь.
Используя алгоритмическую сложность, мы можем систематизировать различные структуры кода, обеспечить соответствующую обратную связь и быстро определить, что неправильно, а что хорошо. Это также позволяет нам увидеть, как производительность меняется с увеличением объема данных, с которыми мы работаем; так как код работает быстро, когда у вас есть десять заказов, но очень медленно, когда у вас есть 1 000 000. Это позволяет нам понять, что нам следует искать в первую очередь, и действия, которые мы должны предпринять, чтобы решить эту проблему.
O(1) — постоянная операция, то есть происходит мгновенно: может быть медленной из-за объема данных, но в то же время хорошо заметной и быстрой по сравнению с другими.
array[0] + array[1];
O(n) — линейный, в целом хитрый: неплох, но не так хорош, как O(1), просто требует внимания, чтобы убедиться, что он принимает простой цикл.
for (const item of array) { sum += item; }
O (n2) — просто плохой и медленный: Цикл в цикле, который в большинстве случаев можно переделать другим способом.
for (let i = 0 , n = array.length; i < n; i++) { for (let j = i + 1; j < n; j++) { sum += array[i] + array[j]; } }
Другой пример выглядит как линейный, поскольку у нас есть один цикл с indexOf, сделанным из O (n2). Поскольку indexOf, как правило, просматривает массив, пока не найдет нужный элемент, то есть зацикливание. Петля в петле в конце.
function foo(array) { for (const item of array) { array[array.indexOf(item)] = item + 20; } }
Еще один пример: у вас есть логика в двух разных функциях, и вы представляете, что эти функции не находятся в одном и том же файле. Это может быть даже результатом фальшивого рефакторинга, когда мы думаем, что разделили логику и делаем правильные вещи.
function myFoo(array) { for (const item of array) { myBar(array, item); } } function myBar(array, item) { array[array.indexOf(item)] = item + 30; }
O(n2) — довольно распространенная и популярная проблема, которую мы можем случайно сделать без предупреждения. Случайно квадратичный — результат такой популярности, и на эту тему есть целый блог.
O(n3) — это просто…
for (let i = 0; n = array.length; i < n; i++) { for (let j = i; j < n; j++) { for(let k = j + 1; k < n; k++) { sum += array[i] + array[j]; } } }
Такой код всегда медленный, несмотря ни на что, и его медлительность увеличивается экспоненциально. Кроме того, в такой логике нет необходимости, а шансы там, где это действительно необходимо, довольно малы, поэтому их можно переделать.
- O(1) — хорошо —сохранить
- O(n) — неплохо —проверьте
- O(n2) — плохо —рефакторинг
- O(n3) — очень плохо —удалить и начать с нуля
Функции Javascript O(n)
Этот список должен помочь вам во время ваших расследований и исследований узких мест, связанных с проблемами производительности. Каждая из этих функций является функцией O(n); следовательно, если он у вас есть в цикле, вы уже заблокировали код, становящийся O (n2).
array.indexOf array.lastIndexOf array.splice array.includes array.reverse array.every array.find array.some array.findIndex array.reduce array.reduceRight array.some Object.values Object.keys
Вышеупомянутая классификация зависит от количества операций, которые должен выполнить движок, в то время как у нас есть другой тип классификации, который зависит от памяти, которую система должна зарезервировать.
Проблемы с производительностью памяти
Неправильное выделение памяти для операции — обычная ситуация в словесном программировании. Мы часто не думаем о выделении памяти для наших операций, что может привести к выделению слишком большого объема памяти и в конечном итоге к проблемам с производительностью. Следовательно, вы всегда должны сначала думать каждый раз, когда видите, что для какого-то процесса выделяется много памяти, и вы всегда должны спрашивать себя — действительно ли мне нужна такая память?
array.slice array.concat array.map array.filter str.split
Описанные выше методы используются для выделения памяти в нашем движке. Когда мы анализируем производительность, на первый взгляд не видно, что такие операции могут повлиять на производительность. Но в то же время, если мы сможем взглянуть на сборщик мусора, мы можем признать, что он может принимать на себя до 70% влияния на производительность.
В какой-то момент движок увидит, что ему не нужно столько памяти, когда он выделяет память для вышеуказанных методов. Двигатель произведет очистку, потому что эта операция является дорогостоящей. Следовательно, влияние сборщика мусора в основном является результатом очистки памяти, которую мы выделили с помощью вышеуказанных методов. Такие действия по очистке связаны с проблемами производительности.
ab.slice().concat(bc).map(myFoo).filter(myBar).reduce(qwerty,0);
Что каждый «функциональный тип» выше в основном делает со структурирующим кодом:
- выделить память для операции slice
- очистить память операции slice
- выделить память для операции concat
- очистить память операции concat
- выделить память для операции map
- очистить память операции map
- выделить память для операции filter
- очистить память операции filter
Четыре выделения и освобождения одновременно
Может быть, вы думаете, что это все еще быстро и не влияет на производительность, но если вы добавите приведенный выше код в какой-нибудь цикл, он сразу же всплывет.
О (журнал п)
Один из самых быстрых типов операций — O(log n). Если, например, у нас есть 1 000 000 объектов, то O(log n) будет равно 20, что гораздо ближе к 1, чем к 1 000 000.
Самый классический пример O(log n) — бинарный поиск.
Следовательно, если мы говорим об алгоритмической оптимизации, мы всегда должны заниматься рефакторингом нашего O(n) до O(log n) и т. д.
Ключевое различие между теорией и практикой заключается в том, как применить это на практике. Классический пример — операция поиска в массиве. Каждый раз, когда мы выполняем поиск в массиве, он выполняется медленно. Первый шаг, который мы должны сделать, это сделать классическую алгоритмическую оптимизацию, — взять данные и реструктурировать их (поместить их в какой-то объект данных, отсортировать их или поместить в другой формат). Основная идея заключается в том, чтобы поместить данные в такой формат, при котором следующие операции позволят нам производить мгновенную обработку данных. Идея состоит в том, чтобы заработать больше в начале, чтобы заработать меньше в конце нескольких действий. В то время как структура данных — это просто способ преобразования данных таким образом, чтобы было легко искать или изменять эту структуру данных.
hash map hash set binary search tree priority heap linked list interval tree grid r-tree quadtree kd-tree // p.s. we don't need to learn it, just try to use one of them and see if it’s useful or not, this is the best way to learn something
Вывод
- Подумайте, как это работает 🤔
- Не бойтесь глубоко погружаться в чей-то код 🧗
- Не бойтесь изобретать велосипед 🚴
- Проверьте открытый исходный код ❤️
- Всегда упрощайте 🍀
- Практика оптимизации 🔄
Вдохновение было взято из оригинальной презентации: