-
Notifications
You must be signed in to change notification settings - Fork 195
Task 1 #153
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?
Task 1 #153
Changes from all commits
ff34018
0b7e05e
0a3476b
ff10bb0
5823c46
d2f6731
11179cf
eac9d46
05ba1ba
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,4 @@ | ||
require_relative 'task-1' | ||
require 'benchmark' | ||
|
||
puts Benchmark.measure { work('data.txt') } |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
# Case-study оптимизации | ||
|
||
## Актуальная проблема | ||
В нашем проекте возникла серьёзная проблема. | ||
|
||
Необходимо было обработать файл с данными, чуть больше ста мегабайт. | ||
|
||
У нас уже была программа на `ruby`, которая умела делать нужную обработку. | ||
|
||
Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время. | ||
|
||
Я решил исправить эту проблему, оптимизировав эту программу. | ||
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. решила* |
||
|
||
## Формирование метрики | ||
Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: | ||
- время выполнения должно быть линейным | ||
- в случае линейности 1_000_000 должен работать не больше 9 секунд | ||
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. это намного продуманнее того что обычно пишут, но не совсем корректно то что ты написала, это условие остановки / бюджет, который получен аппроксимацией линейной Этот вопрос в данном случае tricky. По факту нет простого одного ответа на всю работу. У нас на каждую итерацию оптимизации новая метрика - время работы на файлах разного размера. Когда мы не можем посчитать общую метрику на всю систему / исходную проблему, то мы можем воспользоваться промежуточными метриками. Их функция получается в том, чтобы помочь нам понять, была ли оптимизация успешна на данной итерации. |
||
|
||
## Гарантия корректности работы оптимизированной программы | ||
Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. | ||
|
||
## Feedback-Loop | ||
Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `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. ну я бы тут оценивал сколько у тебя времени уходит на ожидание результата после запуска очередного варианта кода. если это секунд 10-15, то кайф, не нарушается состояние потока |
||
|
||
Вот как я построил `feedback_loop`: | ||
- взять объем, на котором выполнение занимает не больше 5 секунд | ||
- написать тест на перфманс | ||
- воспользоваться профилировщиком | ||
- найти главную точку роста | ||
- оптимизировать | ||
- проверить результат профилировщика | ||
- проверить, что время выполнения уменьшилось | ||
|
||
## Вникаем в детали системы, чтобы найти главные точки роста | ||
Для того, чтобы найти "точки роста" для оптимизации я воспользовался rbspy, rubyprof, stackprof | ||
|
||
Предварительная оценка времени выполнения: | ||
- Время выполнения на 5000: 0.210786 | ||
- Время выполнения на 10000: 0.726360 | ||
- Время выполнения на 20000: 2.843040 | ||
- Время выполнения на 40000: 12.927038 | ||
|
||
рост квадратичный -> O(n^2) | ||
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. 👍 |
||
``` | ||
f(x) ~ 8*10^-9 * x^2 | ||
``` | ||
время выполнения на 3250940: ~23,5 часа | ||
|
||
Вот какие проблемы удалось найти и решить | ||
|
||
### Ваша находка №1 | ||
1) Начнем с объема 20000 (около 3сек), напишем тест на перфманс (не больше 3 секунд) | ||
2) используем профилировщик rbspy | ||
3) по call tree видим, что больше всего времени уходит на блок users.each в методе work (83,8%), а в нем на блок select (58,8%) (```user_sessions = sessions.select { |session| session['user_id'] == user['id'] }```) | ||
4) заменим метод select на предварительную группировку по пользователям. теперь блок users.each занимает 2% времени | ||
5) проверим время на 20000: 0.350901 | ||
5) заменим each + [] << на map. профилировщик не показал блок в значимой статистике | ||
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. это уже второе изменение; очень важно удержитваться от соблазна делать сразу группу изменений, потому что это путь к непониманию того что как сработало и признак того что фидбек луп слишком долго ждать |
||
5) проверим время на 20000: 0.350526 (не изменилось) | ||
6) поправим тест на перфманс (поставим границу 0.4 с) | ||
|
||
### Ваша находка №2 | ||
1) проверим время на 40000: 1.077637, мало | ||
2) проверим 80000: 4.540294 | ||
3) напишем перфоманс тест на 80000 (не больше 4.6 сек) | ||
4) воспользуемся профилировщиком ruby prof flat отчет | ||
5) по отчету видно что больше всего времени занимает конкатенация массивов: | ||
``` | ||
%self total self wait child calls name | ||
30.44 2.073 2.073 0.000 0.000 80200 Array#+ | ||
``` | ||
6) в коде конкатенация используемся при сборке users = [], sessions = [], uniqueBrowsers = [] | ||
7) uniqueBrowsers заменила на map + uniq, users / sessions на append: | ||
``` | ||
%self total self wait child calls name | ||
0.62 0.007 0.007 0.000 0.000 80000 Array#append | ||
``` | ||
8) время на 80000: 0.571369 | ||
9) поправим тест на перфманс (поставим границу 0.6 с) | ||
|
||
### Ваша находка №3 | ||
1) проверим время на 160000: 1.162673, мало | ||
2) проверим время на 320000: 2.476740, мало | ||
3) заметим что сложность стала больше похожа на линейную, чем на квадратичную | ||
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. да-да, после первого же изменения, оно самое критичное, тк там была зарыта квадратичная сложность |
||
3) проверим время на 640000: 5.804469 | ||
4) напишем перфоманс тест на 640000 (не больше 6 сек) | ||
5) воспользуемся профилировщиком stackprof | ||
``` | ||
TOTAL (pct) SAMPLES (pct) FRAME | ||
1304 (23.5%) 1304 (23.5%) (marking) | ||
2634 (47.5%) 705 (12.7%) Object#collect_stats_from_users | ||
``` | ||
что такое (marking) не понятно, но видно что больше всего времени уходит на collect_stats_from_users: | ||
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. marking это одна из фаз GC, будем про это во второй лекции |
||
``` | ||
callers: | ||
2634 ( 100.0%) Object#work | ||
2617 ( 99.4%) Array#each | ||
callees (1929 total): | ||
2634 ( 136.5%) Array#each | ||
1798 ( 93.2%) Object#work | ||
114 ( 5.9%) Hash#merge | ||
``` | ||
6) из кода видно, что метод вызывается 7 раз с разными блоками, каждый раз перебирая заново всех юзеров | ||
7) заметим также что в методе используется не оптимальная конкатенация строк, заменим + на "#{...}" -> 5.212167 c | ||
8) заменим 7 вызовов метода на один вызов с готовым хэшом: | ||
``` | ||
TOTAL (pct) SAMPLES (pct) FRAME | ||
1938 (40.8%) 122 (2.6%) Object#collect_stats_from_users | ||
``` | ||
9) поправим заполнение хэша и его мердж: отдельно преобразуем время и браузеры в переменную и будем их использовть для дальнейшего заполнения, уберем мердж хэшей | ||
``` | ||
было: 1716 (36.1%) 245 (5.2%) Array#map | ||
стало: 1534 (34.1%) 151 (3.4%) Array#map | ||
немного но честная работа | ||
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. много всего в одном шаге |
||
``` | ||
10) проверим время на 640000: 4.317787, не сильный прирост | ||
11) поправим тест на перфманс (поставим границу 4.5 с) | ||
|
||
### Ваша находка №4 | ||
1) продолжим использовать 640000 | ||
2) в топе самых долгих методов остается collect_stats_from_users (42%), посмотрим внутрь - там метод Date.parse (total 25%) | ||
3) заметим, что даты приходят в iso8601, поэтому можно опустить парсинг и сразу использовать | ||
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. пасхалочка |
||
4) таким образом снизили затрачиваемое время в collect_stats_from_users до 19% | ||
5) проверим время на 640000: ~3 | ||
6) поправим тест на перфманс (поставим границу 3.1 с) | ||
7) метод перестал быть главной точкой роста | ||
|
||
### Ваша находка №5 | ||
1) продолжим использовать 640000 | ||
2) в топе методов Array#each (52%), из них 59% вызывается из метода work, в нем 35% - collect_stats_from_users, 22% - parse_session, в нем string split | ||
3) Совокупно string split занимает 14%, уберем лишние вызовы в parse_session | ||
3) теперь string split занимает 11% | ||
4) Array#each все еще главная точка роста, теперь в нем основное время занимает метод collect_stats_from_users | ||
5) рассмотрим его внимательнее: | ||
- перебирается массив user_object, для каждого юзера собирается статистика по его сессиям | ||
- можно внести преобразование типов в методы парсинга | ||
- заменим count на length | ||
- теперь метод collect_stats_from_users занимает 28.0% | ||
6) проверим время на 640000: 2.166889 | ||
6) поправим тест на перфманс (поставим границу 2.5 с) | ||
7) проверим время на 1млн: 3.760000 | ||
8) проверим на полном файле: 17.444283 секунд | ||
9) ради интереса проверим с выключенным gc: ~14 секунд | ||
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. но чисто ради интереса, а то так и память может кончиться |
||
|
||
|
||
## Результаты | ||
В результате проделанной оптимизации наконец удалось обработать файл с данными. | ||
Удалось улучшить метрику системы с ~23ч (предварительная оценка) до ~17.5 сек и уложиться в заданный бюджет. | ||
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. респект за оценку исходного времени! |
||
|
||
## Защита от регрессии производительности | ||
Для защиты от потери достигнутого прогресса при дальнейших изменениях программы были написаны performance тесты на время обработки 80_000 и на линейную зависимость |
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.
О, Лена, привет!