-
Notifications
You must be signed in to change notification settings - Fork 195
Оптимизация первого задания #146
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?
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,16 @@ | ||
source 'https://rubygems.org' | ||
|
||
# ruby-prof | ||
gem 'ruby-prof' | ||
# json | ||
gem 'json' | ||
# pry | ||
gem 'pry' | ||
# date | ||
gem 'date' | ||
# minitest | ||
gem 'minitest' | ||
# rspec-benchmark | ||
gem 'rspec-benchmark' | ||
# rspec | ||
gem 'rspec' |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
GEM | ||
remote: https://rubygems.org/ | ||
specs: | ||
benchmark-malloc (0.2.0) | ||
benchmark-perf (0.6.0) | ||
benchmark-trend (0.4.0) | ||
coderay (1.1.3) | ||
date (3.3.4) | ||
diff-lcs (1.5.1) | ||
json (2.7.2) | ||
method_source (1.1.0) | ||
minitest (5.22.3) | ||
pry (0.14.2) | ||
coderay (~> 1.1) | ||
method_source (~> 1.0) | ||
rspec (3.13.0) | ||
rspec-core (~> 3.13.0) | ||
rspec-expectations (~> 3.13.0) | ||
rspec-mocks (~> 3.13.0) | ||
rspec-benchmark (0.6.0) | ||
benchmark-malloc (~> 0.2) | ||
benchmark-perf (~> 0.6) | ||
benchmark-trend (~> 0.4) | ||
rspec (>= 3.0) | ||
rspec-core (3.13.0) | ||
rspec-support (~> 3.13.0) | ||
rspec-expectations (3.13.0) | ||
diff-lcs (>= 1.2.0, < 2.0) | ||
rspec-support (~> 3.13.0) | ||
rspec-mocks (3.13.1) | ||
diff-lcs (>= 1.2.0, < 2.0) | ||
rspec-support (~> 3.13.0) | ||
rspec-support (3.13.1) | ||
ruby-prof (1.7.0) | ||
|
||
PLATFORMS | ||
arm64-darwin-23 | ||
ruby | ||
|
||
DEPENDENCIES | ||
date | ||
json | ||
minitest | ||
pry | ||
rspec | ||
rspec-benchmark | ||
ruby-prof | ||
|
||
BUNDLED WITH | ||
2.5.9 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
require 'rspec-benchmark' | ||
require_relative 'task-1' | ||
|
||
RSpec.configure do |config| | ||
config.include RSpec::Benchmark::Matchers | ||
end | ||
|
||
describe 'Task' do | ||
describe 'execute less 30 sec' do | ||
let(:file) { 'data_large.txt' } | ||
|
||
it 'success' do | ||
expect { work(File.read(file)) }.to perform_under(30).sec.sample(3).times | ||
end | ||
end | ||
|
||
describe 'linear work' do | ||
it 'success' do | ||
expect do | ||
|n, _i| work(`head -n #{n.to_i} data_large.txt`) | ||
end.to perform_linear.in_range(100, 3_000_000) | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
# Case-study оптимизации | ||
|
||
## Актуальная проблема | ||
В нашем проекте возникла серьёзная проблема. | ||
|
||
Необходимо было обработать файл с данными, чуть больше ста мегабайт. | ||
|
||
У нас уже была программа на `ruby`, которая умела делать нужную обработку. | ||
|
||
Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время. | ||
|
||
Я решил исправить эту проблему, оптимизировав эту программу. | ||
|
||
## Формирование метрики | ||
Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую линейную метрику, чтобы программа отрабатывала за линейное время выполнения в зависимости от размера входных данных. | ||
|
||
## Гарантия корректности работы оптимизированной программы | ||
Программа поставлялась с тестом. Выполнение этого теста в фидбек-лупе позволяет не допустить изменения логики программы при оптимизации. | ||
|
||
## Feedback-Loop | ||
Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за 0.84551 сек. | ||
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`: взял из большого файла первые 1000 строк | ||
head -n 1000 data_large.txt > data1000.txt | ||
отработало за 0.018932 сек. | ||
|
||
Затем взял первые 10000 строк - отработала за 0.84551 сек. | ||
Взял первые 20000 строк - отработала за 3.424572 | ||
Взял первые 30000 строк - отработала за 7.557747 | ||
Взял первые 100000 строк - не дождался, но было точно 100+ сек. | ||
|
||
Таким образом, остановился на 10000 строках и заодно проанаизировал зависимость, котороая являлась степенной или экспоненциальной, как можно было увидеть из примеров выше. | ||
|
||
## Вникаем в детали системы, чтобы найти главные точки роста | ||
|
||
Сначала написал тест на производительность, в файле benchmark.rb. По мере исправлений я менял входные данные для метода work, чтобы не выходить за рамки 30 сек, т.е. сначала улучшил для 100000 строк время отработки, чтобы отрабатывало меньше чем за 30 сек, , зачем для 500000, и т.д. А так же написал тест который проверяет линейную зависимость, в начале, конечно же, который провалился, тем самым подтвердив не линейную зависимость, которая была выявлена еще при построении Feedback-Loop, сообщения теста были следующие | ||
- expected block to perform linear, but performed exponential | ||
- expected block to perform linear, but performed logarithmic | ||
- expected block to perform linear, but performed power | ||
Любые, но не линейная | ||
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. )) но кстати logarithmic - это лучше чем линейная |
||
|
||
Для того, чтобы найти "точки роста" для оптимизации я несколькими воспользовался инструментами, которые были представлены в первой недели лекции. Это FlatPrinter, GraphHtmlPrinter, и самый информативный, на мой взгляд, это CallTreePrinter, так как по построенному дереву можно сразу увидеть не только метод, который дольше всего отрабатывает, но и все дочерние методы в виде дерева в процентах, сколько каждый из них потребляет. Сначала достаточно было более простых профилировщиков - FlatPrinter, GraphHtmlPrinter, но ближе к концу более информативным и более наглядным оказался CallTreePrinter! | ||
|
||
Вот какие проблемы удалось найти и решить | ||
|
||
### Находка №1 | ||
- Flat сразу указал на главную точку роста. Это был метод select, который занимал 62% времени выполнения | ||
- Заменил метод select методом group_by, разбив все сессии пользователей за один проход | ||
- Для 10000 строк стало отрабатывать за 0.196914 сек. - было 0.84551 сек. -> в 4+ раза быстрее | ||
- Отчет профилировщика изменился, исправленная проблема перестала быть главной точкой роста | ||
|
||
### Находка №2 | ||
- Опять же с помощью Flat увидел, что следующей точкой роста стал метод all?, котоый использовался при вычислении уникальных браузеров, вложенная цикличность | ||
- Заменил эту вложенную цикличность each + all? на одиночный проход map и выборкой всех уникальных значений | ||
- Для 10000 строк стало отрабатывать за 0.001084 сек. - было 0.196914 сек. -> в 180 раз? Вышел сильный прирост для небольшого объема данных | ||
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. при профилировании лучше давать программе поработать пару секунд хотя бы, чтобы уменьшить влияние погрешностей и дать покрутиться в основном цикле (если он есть) |
||
- Отчет профилировщика изменился, исправленная проблема перестала быть главной точкой роста | ||
|
||
Так как для 10000 отрабатывает теперь очень быстро, изменил и увеличил Feedback-Loop, теперь стало отрабатывать для 100000 строк за 4.852382 сек, остановился на этом. | ||
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 | ||
- Тем же Flat профилировщиком увидел, что следующей тяжелой операцией является сложение массивов, т.е. метод "+", который вызывается при первом же each, где в массив собираются сессии и пользователи | ||
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. Flat это не профилировщик, а отчёт (просто обращаю внимание) |
||
- Заменил сложение методом "<<", тем самым не создавая каждый раз новый массив из двых старых, а используя одни и те же массивы, дополняя их значениями | ||
- Для 100000 строк стало отрабатывать за 0.960417 сек. - было 4.852382 сек. -> в 5 раз быстрее | ||
- Отчет профилировщика изменился, исправленная проблема перестала быть главной точкой роста | ||
|
||
Снова изменил и увеличил Feedback-Loop, теперь стало отрабатывать для 500000 строк за 10.932996 сек. | ||
|
||
### Находка №4 | ||
- Flat профилировщиком показал, что следующая точка роста - это each. Для более понятной картины воспользовался CallTreePrinter, так как в древовидной структуре вызовов можно увидеть последовательность и понятное процентное соотношение затрат ресурсов конкретных методов. В метододе each вызываются map, однако их много, но из дерева видно, что трудозатратным в них является парсинг даты(Date.parse) | ||
- Заменил Date.parse на Date.strptime, так как при парсе даты .strptime является более оптимальным методом | ||
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. с датами можно вообще ничего не делать, это пасхалочка |
||
- Для 500000 строк стало отрабатывать за 9.19779 сек. - было 10.932996 сек. -> в 1.2 раз быстрее | ||
- Отчет профилировщика изменился, исправленная проблема перестала быть главной точкой роста | ||
|
||
### Находка №5 | ||
- Стал пользоваться связкой Flat + CallTreePrinter. Снова точка роста - это each, в котором складываются массивы(подобная проблема как из шага 3), такое место только одно в коде, где создаются объекты класса User | ||
- Решение такое же, как в шаге 3, заменил сложение методом "<<" | ||
- Для 500000 строк стало отрабатывать за 3.572815 сек. - было 9.19779 сек. -> стало в 2.5 раза быстрее | ||
- Отчет профилировщика изменился, исправленная проблема перестала быть главной точкой роста | ||
|
||
### Находка №6 | ||
- Flat + CallTreePrinter. Видно, что огромный процент выполнения занимает итератор each, значит ищем места, где можно их сократить и оптимизировать проход циклов | ||
- Объединил в один each логику сбора статистики по пользователям. Так как каждый раз для вычисления новой статистики проходим вновь по тем же пользователям, когда можно сделать все за раз | ||
- Для 500000 строк стало отрабатывать за 3.154631 сек. - было 3.572815 сек. -> стало чуть быстрее | ||
- Отчет профилировщика фактически не изменился, исправленная проблема не перестала быть главной точкой роста | ||
|
||
В целом, файл data_large.txt теперь обрабатывается за нужное нам время, около 30 сек. Но выполнил еще один шаг, попробовал улучшить результат | ||
### Находка №7 | ||
- Flat + CallTreePrinter и немного GraphHtml. Точка роста - это по прежнему each, но лучше оптимизировать его, к сожалению, больше не вышло, да и сильно картину профилировщика это не поменяло, но если посмотреть по дереву ниже, то видно, что следующими по затратности идут Class::Date>#strptime, затем String#split, которые заменить так же не вышло, и после чего map'ы, которые вызываются из each, и они занимают некоторый процент выполнения. | ||
- Поправил места, где используются map'ы, поскольку они были написаны не оптимально, и там где можно было бы вызвать всего один раз и выполнить какую - либо операцию над данными за одну итерацию, вызывалось два, из - за чего появлялись лишние итерации. Где - то были лишними, например в случае использования any?, где в блоке сразу можно, извлекая необходимые данные, матчить с нужной строкой | ||
- Для тех же 500000 строк стало отрабатывать за 2.760178 сек. - было 3.154631 сек. -> стало отрабатывать еще чуть быстрее | ||
- Отчет профилировщика так же не изменился, исправленная проблема не перестала быть главной точкой роста | ||
|
||
Конечный результат времени обработки файла data_large.txt получился - 24.783419 сек. | ||
|
||
Тест benchmark.rb проходит успешно, среднее время трех запусков программ с data_large.txt получается меньше 30 сек, а так же проверка на линейность perform_linear - выполняется | ||
|
||
Если оптимизировать далее, то скорее придется переписывать программу, стараясь уменьшать кол - во итераций в программе. | ||
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. Во втором задании можно зайти с другой стороны и получить интересные реузльтаты |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
# Deoptimized version of homework task | ||
|
||
require 'ruby-prof' | ||
require_relative 'task-1' | ||
|
||
RubyProf.measure_mode = RubyProf::WALL_TIME | ||
GC.disable | ||
|
||
result = RubyProf.profile do | ||
work | ||
end | ||
|
||
printer = RubyProf::FlatPrinter.new(result) | ||
printer.print(File.open('reports/flat/flat10000_8.txt', 'w+')) | ||
printer = RubyProf::GraphHtmlPrinter.new(result) | ||
printer.print(File.open('reports/graph/graph10000_8.html', 'w+')) | ||
# printer = RubyProf::CallStackPrinter.new(result) | ||
# printer.print(File.open('reports/callstack10000_1.html', 'w+')) | ||
printer = RubyProf::CallTreePrinter.new(result) | ||
printer.print(path: 'reports/callgrind', profile: 'callgrind') |
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.
Это не "метрика", это асимптотика. Метрика == число, вроде кол-во секунд или кол-во мегабайт, или кол-во IPS, ...