Skip to content
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

[potashin] optimization #141

Open
wants to merge 7 commits into
base: master
Choose a base branch
from

Conversation

potashin
Copy link

No description provided.

@potashin potashin marked this pull request as draft April 28, 2024 23:51
@potashin potashin marked this pull request as ready for review April 29, 2024 13:17
- исправленная проблема перестала быть главной точкой роста.

### Находка №6
- `ruby-prof` в режиме `CallStack` показывает, что точкой роста является `7.20% (15.82%) Array#include? [846230 calls, 846230 total]`
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

вот так чтобы не натыкаться на грабли собственной оптимизации, имеет смысл чуть расширять контекст кода, который нужно оптимизировать (я имею в виду находку №4)? т.е. условно не all?, а все формирование уникальных элементов.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

хм, я бы сказал можно так и так

с одной стороны в 4м шаге ты убрал на тот момент топовую проблему из топа, и перешёл к следующей; тут она опять пробилась в топ - опять поправил - по идее это весьма логично

с другой стороны тут случай, что можно заметить, что есть возможность использовать более подходящую структуру данных/алгоритм, по идее можно эту сразу было сделать (но тут есть риск что-то поломать иногда; но тесты должны подстраховать; но это в случае если они есть)

### Находка №7
- `ruby-prof` в режиме `CallGrind` показывает, что точкой роста является `Object::collect_stats_from_users`-> `Array::map`->`String::upcase`
- поскольку используется только `upcase` версия браузера, при парсинге сессия сразу записываем `upcase` версию. Поскольку не так много видов браузеров относительно общего количества сессий, используем мемоизацию.
- время выполнения программы для 1кк входных данных сократилось с 6.9 до с 6.4c
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

тут интересно, что несмотря на то, что это главная точка роста, время сократилось не так сильно, как на следующем этапе

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

такое возможно из-за GC;

когда мы профлируем CPU, есть такая тонкость; возможно какая-то строчка не тормозит сама по себе, но создаёт много лишних объектов, которые потом наступит время убирать

поэтому если хочешь ускорить время работы максимально, то имеет смысл с обоих сторон смотреть - и по CPU, и по памяти (хотя на первый взгляд память это не про скорость; особенно в этом плане наверно важно кол-во аллоцированных объектов; если их слишком много создаётся и удаляется, то будет тормозить)

@potashin potashin force-pushed the feature/potashin-optimization branch 2 times, most recently from 8cb83b6 to 41177e6 Compare April 29, 2024 13:36
@potashin potashin force-pushed the feature/potashin-optimization branch from 41177e6 to 7dbada5 Compare April 29, 2024 13:37
Copy link
Collaborator

@spajic spajic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@@ -0,0 +1,10 @@
result.json
data*.txt
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

respect за gitignore

надо бы добавить в репу, а то иногда прилетают PR'ы на 200к строк кода

case-study.md Outdated
Я решил исправить эту проблему, оптимизировав эту программу.

## Формирование метрики
Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: время выполнения программы для части данных (сначала 50к, потом 100к).
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

++

тут получается немного tricky

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

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

Плюс если мы понимаем асимптотику (например если она линейная), то мы можем и довольно-таки обоснованно прикинуть, что для примера 1/100 данных должна обрабатываться за 300 мс, и тогда вероятно общее время выполнения будет около 30с

Хорошо подбирать объём данных так, чтобы программа успевала покрутиться пару секунд. Если она завершается слишком быстро (“не успевает поработать”) могут возникнуть какие-то перекосы (например, на полном объёме основная часть времени тратится в основном цикле, а если данных мало - то большая часть уходит на инициализацию и финализацию, например на чтение из файла и запись потом в файл)

И плюс когда время уже на миллисекунды - больше влияние погрешностей.

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

## Feedback-Loop
Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось*
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*это был плейсхолдер*

- rbspy показал `83.55 83.55 block (2 levels) in work - task-1.rb:101`: вызов `sessions.filter {}` на каждой итерации по `users.each`;
- перед `users.each` сгруппировал `sessions_by_user = sessions.group_by { |session| session['user_id'] }`, в `each` использовал как `sessions_by_user[user['id']] || []`
- время выполнения программы для 100к входных данных сократилось с 115с до 4с
- исправленная проблема перестала быть главной точкой роста, rbspy показал, что теперь это `98.49 100.00 block in work - task-1.rb:56`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

и самое главное асимптотика уже стала из квадратичной - линейной

- исправленная проблема перестала быть главной точкой роста, rbspy показал, что теперь это `98.49 100.00 block in work - task-1.rb:56`

### Находка №2
- stackprof cli показал `7126 (99.4%) 11 (0.2%) Array#each`, он вызывается несколько раз, наибольшее `6504 ( 91.3%) Object#work]`. Поскольку rbspy указывал на `task-1.rb:56`, что является `end` `each` блока, пробую вынести этот`each` в отдельный метод `parse_file`и подтвердить гипотезу, которая и подтверждается: `5765 (99.8%) 5525 (95.7%) Object#parse_file`. Теперь нужно разобраться, какая именно операция в этом блоке `each` требует оптимизации, `stackprof stackprof.dump --method Object#parse_file` показывает, что это заполнение массива сессий: `5261 (93.2%) / 5133 (90.9%) | 52 | sessions = sessions + [parse_session(line)] if cols[0] == 'session'`.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

- исправленная проблема перестала быть главной точкой роста.

### Находка №6
- `ruby-prof` в режиме `CallStack` показывает, что точкой роста является `7.20% (15.82%) Array#include? [846230 calls, 846230 total]`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

хм, я бы сказал можно так и так

с одной стороны в 4м шаге ты убрал на тот момент топовую проблему из топа, и перешёл к следующей; тут она опять пробилась в топ - опять поправил - по идее это весьма логично

с другой стороны тут случай, что можно заметить, что есть возможность использовать более подходящую структуру данных/алгоритм, по идее можно эту сразу было сделать (но тут есть риск что-то поломать иногда; но тесты должны подстраховать; но это в случае если они есть)

### Находка №7
- `ruby-prof` в режиме `CallGrind` показывает, что точкой роста является `Object::collect_stats_from_users`-> `Array::map`->`String::upcase`
- поскольку используется только `upcase` версия браузера, при парсинге сессия сразу записываем `upcase` версию. Поскольку не так много видов браузеров относительно общего количества сессий, используем мемоизацию.
- время выполнения программы для 1кк входных данных сократилось с 6.9 до с 6.4c
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

такое возможно из-за GC;

когда мы профлируем CPU, есть такая тонкость; возможно какая-то строчка не тормозит сама по себе, но создаёт много лишних объектов, которые потом наступит время убирать

поэтому если хочешь ускорить время работы максимально, то имеет смысл с обоих сторон смотреть - и по CPU, и по памяти (хотя на первый взгляд память это не про скорость; особенно в этом плане наверно важно кол-во аллоцированных объектов; если их слишком много создаётся и удаляется, то будет тормозить)

case-study.md Outdated

## Результаты
В результате проделанной оптимизации наконец удалось обработать файл с данными.
Удалось улучшить метрику системы на 100к с 115с до 5с секунд и уложиться в заданный бюджет. Для полного файла время выполнение стало 24с.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

case-study.md Outdated
Удалось улучшить метрику системы на 100к с 115с до 5с секунд и уложиться в заданный бюджет. Для полного файла время выполнение стало 24с.

## Защита от регрессии производительности
Для защиты от потери достигнутого прогресса при дальнейших изменениях программы добавил два теста: прогон на 100к данных до 5 секунд, проверка на линейную асимптотику на основе данных от 1000 до 100000 записей.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@potashin potashin force-pushed the feature/potashin-optimization branch from d36a77e to f6610b9 Compare May 1, 2024 08:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants