-
Notifications
You must be signed in to change notification settings - Fork 195
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
Optimize task 1 #152
base: master
Are you sure you want to change the base?
Optimize task 1 #152
Changes from all commits
cf46dce
9421d00
a133512
f758572
2fdb832
04157e2
6288d40
3e0b622
87d4790
2a98c68
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,6 @@ | ||
data_large.txt | ||
data_small.txt | ||
result.json | ||
|
||
/ruby_prof_reports | ||
/stackprof_reports |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
require 'rspec/core' | ||
require 'rspec-benchmark' | ||
|
||
require_relative 'task-1_with_argument.rb' | ||
|
||
RSpec.configure do |config| | ||
config.include RSpec::Benchmark::Matchers | ||
end | ||
|
||
describe 'Performance' do | ||
before do | ||
`head -n #{8000} data_large.txt > data_small.txt` | ||
end | ||
it 'works under 1 ms' do | ||
expect { | ||
work('data_small.txt') | ||
}.to perform_under(1000).ms.warmup(2).times.sample(10).times | ||
end | ||
|
||
let(:measurement_time_seconds) { 1 } | ||
let(:warmup_seconds) { 0.2 } | ||
it 'works faster than 1 ips' do | ||
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. 👍 |
||
expect { | ||
work('data_small.txt') | ||
}.to perform_at_least(1).within(measurement_time_seconds).warmup(warmup_seconds).ips | ||
end | ||
|
||
it 'works with data_large under 35sec' do | ||
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. для реального теста конечно слишком много; сейчас пришла идея, что в принципе можно даже закомитить что-то подобное в рабочую репу, но добавить например ENV-переменную вроде PERF_TEST и запускать такие тесты только если ENV[PERF_TEST] == 'true' |
||
expect { | ||
work('data_large.txt') | ||
}.to perform_under(35).sec.warmup(2).times.sample(10).times | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
require 'benchmark' | ||
require 'benchmark/ips' | ||
require_relative 'task-1_with_argument.rb' | ||
|
||
COUNTERS = [1, 2, 4, 8, 16, 32, 64, 128, 256] | ||
|
||
COUNTERS.each do |counter| | ||
time = Benchmark.realtime do | ||
`head -n #{counter*1000} data_large.txt > data_small.txt` | ||
work('data_small.txt') | ||
end | ||
puts "Finish in #{time.round(2)}" | ||
end | ||
|
||
# initial | ||
|
||
# 1000 - Finish in 0.03 | ||
# 2000 - Finish in 0.13 | ||
# 4000 - Finish in 0.31 | ||
# 8000 - Finish in 0.97 | ||
# 16000 - Finish in 3.98 | ||
# 32000 - Finish in 22.26 | ||
|
||
|
||
Benchmark.ips do |x| | ||
x.config( | ||
stats: :bootstrap, | ||
confidence: 95, | ||
) | ||
|
||
x.report("work") do | ||
`head -n #{128000} data_large.txt > data_small.txt` | ||
work('data_small.txt') | ||
end | ||
end | ||
|
||
# initial | ||
|
||
# work 0.236 (± 1.4%) i/s | ||
|
||
time = Benchmark.realtime do | ||
work('data_large.txt') | ||
end | ||
puts "Finish in #{time.round(2)}" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
# Case-study оптимизации | ||
|
||
## Актуальная проблема | ||
В нашем проекте возникла серьёзная проблема. | ||
|
||
Необходимо было обработать файл с данными, чуть больше ста мегабайт. | ||
|
||
У нас уже была программа на `ruby`, которая умела делать нужную обработку. | ||
|
||
Она успешно работала на файлах размером пару мегабайт, но для большого файла она работала слишком долго, и не было понятно, закончит ли она вообще работу за какое-то разумное время. | ||
|
||
Я решил исправить эту проблему, оптимизировав эту программу. | ||
|
||
## Формирование метрики | ||
Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: Файл размером 3250940 строк должен обрабатываться за 30 секунд. | ||
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`, который позволил мне получать обратную связь по эффективности сделанных изменений за (*время, которое у вас получилось*) (Разное время для каждого случая, сперва пять минут, когда точки роста очевидны потом дольше приходилось вникать) | ||
|
||
Вот как я построил `feedback_loop`: | ||
1. Создала копию рабочего файла, которая принимает аргументом файл меньшего размера. | ||
2. Бенчмаркинг. Создала файл benchmark.rb для проверки времени выполнения программы на меньших данных и проверки ips на 16000 строк. | ||
3. Assert performance. Создала файл assert_performance_spec.rb для тестирования времни выполнения и ips. | ||
4. Профилирование. Создала файлы ruby-prof.rb и stackprof.rb для генерации отчетов. | ||
|
||
## Вникаем в детали системы, чтобы найти главные точки роста | ||
Для того, чтобы найти "точки роста" для оптимизации я воспользовался отчетами falt, graph и callstack от ruby-prof и cli stackprof. | ||
|
||
Вот какие проблемы удалось найти и решить | ||
|
||
### Ваша находка №1 | ||
- Все отчеты показали главную точку роста Array#select (59.64% по ruby-prof flat) | ||
- Вместо перебора сессий в select создала массив sessins_hash c ключом user_id | ||
- На 16000 строк ips увеличился с 0.236 до 2.581 | ||
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. главное, что асимптотику поправили |
||
- Проблема перестала быть точкой роста | ||
|
||
### Ваша находка №2 | ||
- Все отчеты показывают точку роста в Array#all? (24,56% по ruby-prof flat) | ||
- Вместо метода all? != использовала uniq | ||
- На 16000 строк ips увеличился с 2.581 до 3.203 | ||
- Проблема перестала быть точкой роста | ||
|
||
### Ваша находка №3 | ||
- callstack отчет показывает точку роста в collect_stats_from_users, а конкретно в Date#parse | ||
- Я решила сделать сортировку даты без парсинга | ||
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. 👍 да-да, это пасхалочка |
||
- На 16000 строк ips увеличился с 3.203 до 4.265 | ||
- Проблема перестала быть точкой роста | ||
|
||
### Ваша находка №4 | ||
- callstack отчет показывает точку роста в Array#each, а конкретно в Array#+ | ||
- Вместо создания нового массива на Array#+ я решила добавлять элементы в массив << | ||
- На 16000 строк ips увеличился с 4.265 до 7.329 | ||
- Проблема перестала быть точкой роста | ||
|
||
### Ваша находка №5 | ||
- в этом месте кажется отчеты показывают разное, но я сосредоточилась на отчете ruby-prof flat и ruby-prof graph, которые показывают точну роста в tring#split (13.11%) | ||
- Передаю в методы parse_user и parse_session массивы вместо строк, т.к. split уже сделан в методе, который их вызывает | ||
- На 16000 строк ips увеличился с 7.329 до 7,640 | ||
- Проблема все еще имеет высокий процент, но он снизился до (8.84%) | ||
|
||
На этом моменте я решила использовать для бенчмаркинга и профилирования большие величины, чтобы увеличить точность. | ||
На 128_000 строк ips - 0.867i/s | ||
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. ips лучше подходит для микро-бенчмарков, где много итераций в секунду у нас тут скорее ситуация, когда надо много секунд на одну итерацию, поэтому проще в секундах считать |
||
|
||
### Ваша находка №6 | ||
- Отчеты показывают точку роста в Object#collect_stats_from_users, а конкретно в Array#map ( 16.50%) | ||
- В нескольких вызовах collect_stats_from_users вызывается map на массиве сессий. Объединяют эти вызовы в один. | ||
- 128_000 строк ips увеличился с 0.867 до 1.147 | ||
- collect_stats_from_users уменьшился с 56,19% до 41.28% | ||
|
||
На этом этапе проверяю, сколько выполняется программа на большом файле - 43.88 и 17.25 со отключенным GC | ||
|
||
### Ваша находка №7 | ||
- ruby-prof flat показывает точку роста в Array#map | ||
- Несколько раз вызываются лишние map, убрала их. Так же заменила метод поиска уникальных браузеров на Set | ||
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. 👍 |
||
- 128_000 строк ips увеличился с 1.147 до 1.265 | ||
- Array#map уменьшился с 17% до 10.93% | ||
Общее время выполения на большом файле - 36.78 секунды | ||
|
||
После этого не вижу в отчетах ничего, что могло бы мне помочь. В слепую изменяю пару методов, т.к. все еще не укладываюсь в метрику. Удалось оптимизировать время выполнения на большом файле до 33.65 секунд. | ||
|
||
## Результаты | ||
В результате проделанной оптимизации наконец удалось обработать файл с данными. | ||
Удалось улучшить метрику системы с до 33.65 что почти укладывается в заданный бюджет. | ||
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. это конечно от компа зависит; в целом вроде у вас основное всё сделано чего обычно хватает для победы есть шанс что во втором задании при подходе к этой проблеме с другой стороны получиться уложиться в 30 сек! |
||
|
||
|
||
## Защита от регрессии производительности | ||
Для защиты от потери достигнутого прогресса при дальнейших изменениях программы написала performance spec, проверяющий, что на большом файле работа выполняется менее 35 секунд на 10 сэмплах. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
# RubyProf Flat report | ||
# ruby 12-ruby-prof-flat.rb | ||
# cat ruby_prof_reports/flat.txt | ||
require 'ruby-prof' | ||
require_relative 'task-1_with_argument.rb' | ||
|
||
RubyProf.measure_mode = RubyProf::WALL_TIME | ||
`head -n #{128000} data_large.txt > data_small.txt` | ||
|
||
result = RubyProf.profile do | ||
work("data_small.txt") | ||
end | ||
|
||
flat_printer = RubyProf::FlatPrinter.new(result) | ||
flat_printer.print(File.open("ruby_prof_reports/flat.txt", "w+")) | ||
|
||
graph_printer = RubyProf::GraphHtmlPrinter.new(result) | ||
graph_printer.print(File.open("ruby_prof_reports/graph.html", "w+")) | ||
|
||
printer_callstack = RubyProf::CallStackPrinter.new(result) | ||
printer_callstack.print(File.open('ruby_prof_reports/callstack.html', 'w+')) | ||
|
||
# printer_calltree = RubyProf::CallTreePrinter.new(result) | ||
# printer_calltree.print(:path => "ruby_prof_reports", :profile => 'callgrind') |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
# Stackprof report | ||
# ruby 16-stackprof.rb | ||
# cd stackprof_reports | ||
# stackprof stackprof.dump | ||
# stackprof stackprof.dump --method Object#work | ||
require 'json' | ||
require 'stackprof' | ||
require_relative 'task-1_with_argument.rb' | ||
|
||
`head -n #{8000} data_large.txt > data_small.txt` | ||
|
||
StackProf.run(mode: :wall, out: 'stackprof_reports/stackprof.dump', interval: 1000) do | ||
work("data_small.txt") | ||
end | ||
|
||
profile = StackProf.run(mode: :wall, raw: true) do | ||
work("data_small.txt") | ||
end | ||
|
||
File.write('stackprof_reports/stackprof.json', JSON.generate(profile)) |
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.
👍