Skip to content

Оптимизация первого задания #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

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions Gemfile
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'
50 changes: 50 additions & 0 deletions Gemfile.lock
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
24 changes: 24 additions & 0 deletions benchmark.rb
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
97 changes: 97 additions & 0 deletions case-study.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Case-study оптимизации

## Актуальная проблема
В нашем проекте возникла серьёзная проблема.

Необходимо было обработать файл с данными, чуть больше ста мегабайт.

У нас уже была программа на `ruby`, которая умела делать нужную обработку.

Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время.

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

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

Choose a reason for hiding this comment

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

Это не "метрика", это асимптотика. Метрика == число, вроде кол-во секунд или кол-во мегабайт, или кол-во IPS, ...


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

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

Choose a reason for hiding this comment

The 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
Любые, но не линейная
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 раз? Вышел сильный прирост для небольшого объема данных
Copy link
Collaborator

Choose a reason for hiding this comment

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

при профилировании лучше давать программе поработать пару секунд хотя бы, чтобы уменьшить влияние погрешностей и дать покрутиться в основном цикле (если он есть)

- Отчет профилировщика изменился, исправленная проблема перестала быть главной точкой роста

Так как для 10000 отрабатывает теперь очень быстро, изменил и увеличил Feedback-Loop, теперь стало отрабатывать для 100000 строк за 4.852382 сек, остановился на этом.
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍


### Находка №3
- Тем же Flat профилировщиком увидел, что следующей тяжелой операцией является сложение массивов, т.е. метод "+", который вызывается при первом же each, где в массив собираются сессии и пользователи
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 является более оптимальным методом
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 - выполняется

Если оптимизировать далее, то скорее придется переписывать программу, стараясь уменьшать кол - во итераций в программе.
Copy link
Collaborator

Choose a reason for hiding this comment

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

Во втором задании можно зайти с другой стороны и получить интересные реузльтаты

20 changes: 20 additions & 0 deletions profilling.rb
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')
Loading