-
Notifications
You must be signed in to change notification settings - Fork 195
task1 #160
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
task1 #160
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
# Case-study оптимизации | ||
|
||
## Актуальная проблема | ||
В нашем проекте возникла серьёзная проблема. | ||
|
||
Необходимо было обработать файл с данными, чуть больше ста мегабайт. | ||
|
||
У нас уже была программа на `ruby`, которая умела делать нужную обработку. | ||
|
||
Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время. | ||
|
||
Я решил исправить эту проблему, оптимизировав эту программу. | ||
|
||
## Формирование метрики | ||
Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: Время обработки текстового файла, содержащего 3,25 млн. строк. Бюджет данной метрики равен 30 секунд. | ||
|
||
## Гарантия корректности работы оптимизированной программы | ||
Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. | ||
|
||
## Feedback-Loop | ||
Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за ~48 секунд. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
|
||
Вот как я построил `feedback_loop`: | ||
- Определение оптимального набора данных для профилирования | ||
- Определение главной точки роста с помощью профилирования | ||
- Нахождение в коде проблемного места, связанного с главной точкой роста | ||
- Внесение в код изменений, касающихся оптимизации только главной точки роста | ||
- Повторное профилирование и определение новой главной точки роста | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. по идее ещё замеры метрики до и после изменения, чтобы понимать какой эффект это изменение дало |
||
|
||
## Вникаем в детали системы, чтобы найти главные точки роста | ||
Для того, чтобы найти "точки роста" для оптимизации я воспользовался библиотекой ruby-prof с различными вариантами визуализации отчета. | ||
|
||
Вот какие проблемы удалось найти и решить | ||
|
||
### Array#select | ||
- Начал профилирования с объема данных в 20000 строк. С таким объемом данных программа выполнялась ~25 секунд, что является достаточным для определения главной точки роста. | ||
- После сделанных замеров и анализа отчетов, получилось, что основной точкой роста является вызов метода select у массива. | ||
- В коде select вызывается только в одном месте для получения сессий пользователя внутри цикла по пользователям. | ||
- Я решил отказаться от этого метода, немного изменив хранение данных. В первом цикле по строкам файла, сессии я заполняю в новом хэше, ключами которого являются id пользователей. Таким образом, в цикле по заполнению массива объектов пользователей, получить сессии можно обратившись по ключу к хэшу. | ||
- После сделанных изменений проблема перестала быть основной точкой роста и программа стала выполняться в ~10 раз быстрей, т.е. выбранная метрика улучшилась в 10 раз. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. главное, что исправили сложность с O(N^2) на O(N) |
||
|
||
### Много вызовов добавления элементов в массив | ||
- Следующей точкой роста (~25% времени) была операция Array#+, т.е. добавление элемента в массив. Причем, на объеме данных в 20000 строк, эта операция выполнялась > 40000 раз. | ||
- Я проанализировал код на предмет избыточных циклов. Обнаружил, что от цикла, заполняющего массив уникальных браузеров можно избавиться и вычислить все уникальные браузеры еще в первом цикле по строкам файла. Реализовал я это с помощью множества (Set). Также, на этом же шаге оптимизации я изменил расчет параметра allBrowsers в итоговом отчете, заменив получение браузеров из массива сессий на множество. | ||
- Метрика улучшилась больше, чем в 2 раза, файл с 20000 строками стал обрабатываться за 1.1 сек. | ||
- Точка роста осталась прежней, но уменьшилось количество вызовов добавления в массив | ||
|
||
### Вместо добавления элементов в массив, можно присваивать значения по индексу | ||
- Основная точка роста осталась операция Array#+. Я предположил, что на больших массивах быстрей будет операция присваивания по индексу, чем добавление нового элемента, т.к. в последнем случае интерпретатору нужно регулярно расширять занимаемую память массивом, на что тратится дополнительное время. Если массив изначально будет задан с определенным количеством элементов, то наполнение этого массива данными по индексу должно быть быстрей | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. идея подготовить массивы правильного размера классная! но Array#+ это скорее про то что используется форма a = a + [b], а надо a << b |
||
- Перед первым циклом, инициализируются массивы пользователей и сессий размером, равным количеству строк в файле. Такой размер избыточен, т.к. часть данных в файле являются сессиями, а часть - пользователями. Поэтому, после заполнения массивов, вызывается метод compact! для каждого из массивов. | ||
- Метрика улучшилась еще примерно в ~2 раза, 20000 строк обработались менее чем за 1 сек. Поэтому, для более точного профилирования пришлось увеличить выборку строк до 100000. | ||
- Проблема решилась, после профилирования на 100000 строк, основной точкой роста стала операция Array#each | ||
|
||
### Много циклов перебора элементов массива | ||
- Основной точкой роста стала операция Array#each, вызывающаяся 9 раз. Исходя из логики программы, более чем достаточно пройти по всем элементам всего 2 раза - сначала, для чтения данных из файла, затем, для заполнения итогового отчета. Но алгоритм реализован так, что вторым циклом заполняется массив объектов пользователей с нужными атрибутами, а потом, для 7 статистических показателей отчета производится итерирование по массиву объектов класса User. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Array#each, который вызывается 9 раз с блоками == не понятно на самом деле в чём проблема Можно было бы например попробовать другие отчёты, либо порефакторить код (заменить анонимные блоки на именованые методы), чтобы понять более точно где проблема на самом деле |
||
- Было принято решение отказаться вообще от массива объектов пользователей и 7 статистических показателей вычислять во втором цикле по массиву users. Это стало возможным, т.к. на предыдущих шагах оптимизации я сделал хэш, из которого в каждой итерации по массиву users, можно получить по ключу сессии пользователя. На этом шаге оптимизации я избавился от класса User и метода collect_stats_from_users. | ||
- Метрика улучшилась на ~30%, программа обработала файл со 100000 строками за ~2.2 сек. | ||
- Количество вызовов Array#each сократилось до 2, но при профилировании эта операция всё равно осталась по продолжительности на первом месте. Так произошло, потому что после проделанных изменений, все вычисления оказались внутри двух циклов. Поэтому, основную точку роста следует искать в дочерних вызовах. Такой получилась операция Array#map с 12% времени выполнения. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. да-да, дело-то не в each, а в блоках, которые выполняются |
||
|
||
### Много вызовов Array#map | ||
- Основной точкой роста является метод map для массива. Количество вызовов этого метода составило ~170000 при общем количестве обрабатываемых строк 100000. На мой взгляд, такое количество избыточно и нужно попытаться оптимизировать код для их уменьшения. | ||
- Всё использование map находится во втором цикле, когда рассчитывается статистика пользователя для итогового отчета. При этом, всю эту статистику можно посчитать еще на первом цикле, расширив количество значений хэша, в котором хранятся сессии. Тогда во втором цикле, можно будет только обратиться к хэшу и получить уже готовые значения. | ||
- Метрика улучшилась еще на ~25% | ||
- Проделанные изменения позволили полностью избавиться от вызова map, и этот метод перестал быть основной точкой роста. | ||
|
||
### Долгий парсинг даты | ||
- Профилирование на 100000 строк показало, что основной точкой роста является парсинг даты (<Class::Date>#parse). | ||
- Анализ исходных данных показал, что парсинг даты в целом не нужен, т.к. в исходных данных дата уже представлена в нужном формате iso8601. Сортировка строковых значений даст тот же эффект, что и сортировка объектов Date. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
- Полностью убрал парсинг даты, метрика улучшилась на ~60%. Обработка 100000 строк стала выполняться за ~1.5 сек. | ||
- Дальнейшее профилирование производилось на полном объеме данных в ~3.2 млн. строк. | ||
|
||
### Избыточный вызов split | ||
- Основной точкой роста стал вызов метода split, причем количество вызовов превышало в 2 раза количество строк в файле, хотя по логике программы должно совпадать. Это произошло потому, что в методы parse_user и parse_session передается строка, а не массив, полученный в начале цикла методом split. | ||
- В метод parse_user я стал передавать массив, а от метода parse_session решил избавиться и производить заполнение хэша из массива col. | ||
- Метрика улучшилась еще на ~8%. Обработка полного файла составила ~48.5 сек. | ||
- Основной точкой роста остался метод String#split, который вызывается столько раз, сколько строк в исходном файле. | ||
|
||
## Результаты | ||
В результате проделанной оптимизации наконец удалось обработать файл с данными. | ||
Удалось улучшить метрику системы с 25 сек. на файле 20000 строк до 48 сек. на файле с 3 млн строк и почти уложиться в заданный бюджет. | ||
|
||
## Защита от регрессии производительности | ||
Для защиты от потери достигнутого прогресса при дальнейших изменениях программы с помощью библиотеки rspec-benchmark был написан тест на производительность, который выдаст ошибку, если значение выбранной метрики превысит 50 сек. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ды, но мы ещё формируем промежуточные метрики по дороге. Мы их используем чтобы понять насколько успешным было очередное изменение на очередной итерации. Это нормально, что мы используем новую метрику на разных шагах большого процесса.