-
Notifications
You must be signed in to change notification settings - Fork 15
Cpu optimizations #9
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
cb7ecb0
b9f5f3e
965e22a
5f8919f
68206cb
0bd6f2b
36afe99
b595fff
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,3 @@ | ||
| data/* | ||
| result.json | ||
| ruby_prof_flat.txt |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| require './task-2.rb' | ||
| require 'benchmark/ips' | ||
|
|
||
| Benchmark.ips do |bench| | ||
| bench.config(stats: :bootstrap, confidence: 99) | ||
| bench.warmup = 0 | ||
| bench.report('Process 1Mb') { work('data/data_1MB.txt') } | ||
| bench.report('Process 4Mb') { work('data/data_4MB.txt') } | ||
| bench.report('Process 10Mb') { work('data/data_10MB.txt') } | ||
|
|
||
| bench.compare! | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,4 @@ | ||
| # Case-study оптимизации | ||
| # Case-study оптимизации, часть 2 [(часть 1)](https://github.com/KirkovAlexey/task-1/blob/master/case-study-template.md) | ||
|
|
||
| ## Актуальная проблема | ||
| В нашем проекте возникла серьёзная проблема. | ||
|
|
@@ -12,35 +12,149 @@ | |
| Я решил исправить эту проблему, оптимизировав эту программу. | ||
|
|
||
| ## Формирование метрики | ||
| Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: *тут ваша метрика* | ||
| Для того, чтобы понимать, дают ли мои изменения положительный эффект на быстродействие программы я придумал использовать такую метрику: | ||
|
|
||
| - Измерять количество потребляймой памяти | ||
| - Измерять количество затраченного времени | ||
|
|
||
| ## Гарантия корректности работы оптимизированной программы | ||
| Программа поставлялась с тестом. Выполнение этого теста позволяет не допустить изменения логики программы при оптимизации. | ||
|
|
||
| ## Асимптотика | ||
|
|
||
| Для упрощения работы с данными подготовил программу `preparing-data.rb`, которая генерит данные нескольких размеров (1mb, 4Mb, 10Mb и оригинальный файл) из архива. | ||
|
|
||
| Для просмотра метрик я использую программу `asymptotics.rb`, которая замеряет метрики для данных разного объема. | ||
|
|
||
| До начала оптимизации были следующие параметры | ||
|
|
||
| ``` shellsession | ||
|
|
||
| Calculating ------------------------------------- | ||
| 3.658 (± 1.7%) i/s - 19.000 in 5.200075s | ||
| 0.938 (± 3.6%) i/s - 5.000 in 5.337153s | ||
| 0.379 (± 3.2%) i/s - 2.000 in 5.289088s | ||
| with 99.0% confidence | ||
|
|
||
| Comparison: | ||
| Process 1Mb: 3.7 i/s | ||
| Process 4Mb: 0.9 i/s - 3.90x (± 0.16) slower | ||
| Process 10Mb: 0.4 i/s - 9.66x (± 0.44) slower | ||
| with 99.0% confidence | ||
|
|
||
| ``` | ||
|
|
||
| ## Feedback-Loop | ||
| Для того, чтобы иметь возможность быстро проверять гипотезы я выстроил эффективный `feedback-loop`, который позволил мне получать обратную связь по эффективности сделанных изменений за *время, которое у вас получилось* | ||
|
|
||
| Вот как я построил `feedback_loop`: *как вы построили feedback_loop* | ||
| Вот как я построил `feedback_loop`: | ||
|
|
||
| - Используя асимптотику я выбрал минимальный размер файла 1 Mb, который позволял успешно выполится программе без оптимизации | ||
| - Использований метрик по времени выполения и памяти | ||
| - Поиск возможных точек роста | ||
| - Оптимизация кода | ||
| - Повторный запуск программы и анализ метрик | ||
|
|
||
| ## Вникаем в детали системы, чтобы найти 20% точек роста | ||
| Для того, чтобы найти "точки роста" для оптимизации я воспользовался *инструментами, которыми вы воспользовались* | ||
| Для того, чтобы найти "точки роста" для оптимизации я воспользовался следующими инструментами: | ||
|
|
||
| `benchmark`, `memory_profiler`, `ruby-prof`, `benchmark-ips`, `valgrind` | ||
|
|
||
| Вот какие проблемы удалось найти и решить | ||
|
|
||
| ``` shellsession | ||
|
|
||
| %self total self wait child calls name | ||
| 16.15 0.354 0.068 0.000 0.286 3 Array#each | ||
| 10.94 0.046 0.046 0.000 0.000 22614 Array#include? | ||
| 10.81 0.045 0.045 0.000 0.000 26693 String#split | ||
| 10.33 0.047 0.043 0.000 0.004 22614 <Class::Date>#strptime | ||
| 7.64 0.138 0.032 0.000 0.106 22614 Object#parse_session | ||
| 7.23 0.057 0.030 0.000 0.027 1 JSON::Ext::Generator::GeneratorMethods::Hash#to_json | ||
| 4.98 0.021 0.021 0.000 0.000 67478 String#encode | ||
| 3.58 0.015 0.015 0.000 0.000 22614 Date#iso8601 | ||
| 3.45 0.014 0.014 0.000 0.000 8156 Array#map | ||
| 2.56 0.011 0.011 0.000 0.000 17028 String#=~ | ||
| 2.34 0.010 0.010 0.000 0.000 68242 Array#last | ||
| 2.08 0.009 0.009 0.000 0.000 8157 Array#sort | ||
| ``` | ||
|
|
||
| Метрика была получена с помощью `ruby-prof` в режиме `FlatPrinter` | ||
|
|
||
| ### Ваша находка №1 | ||
| О вашей находке №1 | ||
|
|
||
| Аналируя метрику я увидел, что на четвертой строчке стоит парсинг даты. | ||
| В прошлой части я уже изменил парсинг даты с `Date.parse` на `Date.strptime`, это принесло результат. | ||
| В этот раз я обнаружил, что формат даты изначально совпадает с форматом `iso8601` и это означает что возможно удалить избытоное преобразование. | ||
|
|
||
| Метод после изменения: | ||
|
|
||
| ``` shellsession | ||
| def parse_session(session) | ||
| fields = session.split(COMMA) | ||
| { | ||
| 'user_id' => fields[1], | ||
| 'session_id' => fields[2], | ||
| 'browser' => fields[3].upcase, | ||
| 'time' => fields[4].to_i, | ||
| 'date' => fields[5] | ||
| } | ||
| end | ||
| ``` | ||
|
|
||
| После удаления избыточного преобразования время выполнения метода `parse_session` уменьшилось. | ||
| Вместо 32.72% стало 21.71% из 100% времени работы скрипта. | ||
|
|
||
| Профилировка выполнялась помощью `ruby-prof` в режиме `GraphHtmlPrinter` | ||
|
|
||
| ``` shellsession | ||
| 32.72% 7.62% 0.14 0.03 0.00 0.10 22614 Object#parse_session 32 | ||
| 0.04 0.04 0.00 0.00 22614/22614 <Class::Date>#strptime 34 | ||
| 0.03 0.03 0.00 0.00 22614/26693 String#split 32 | ||
| 0.02 0.02 0.00 0.00 22614/22614 Date#iso8601 34 | ||
| 0.01 0.01 0.00 0.00 22614/22614 String#upcase 34 | ||
| 0.01 0.01 0.00 0.00 22614/22614 String#to_i 34 | ||
| ``` | ||
|
|
||
| ``` shellsession | ||
| 21.71% 7.04% 0.07 0.02 0.00 0.05 22614 Object#parse_session 32 | ||
| 0.04 0.04 0.00 0.00 22614/26693 String#split 32 | ||
| 0.01 0.01 0.00 0.00 22614/22614 String#upcase 34 | ||
| 0.00 0.00 0.00 0.00 22614/22614 String#to_i 34 | ||
| ``` | ||
|
|
||
| ### Ваша находка №2 | ||
| О вашей находке №2 | ||
|
|
||
| ### Ваша находка №X | ||
| О вашей находке №X | ||
| По отчету видно что второе место по затратам занимает метод `Array#include?`, который используется при формировании массива с сессиями юзеров. | ||
| При анализе исходного кода видно, что мы можем использовать `SortedSet`, так как это нам даст выигрыш в скорости при поиске элемента в массиве и не будет необходимости сортировать массив для статистики. | ||
|
Owner
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. 👍 |
||
|
|
||
| Замеры при помощи `ruby-prof` в режиме `GraphHtmlPrinter` показали прирост времени выполения с `0.33529114723205566` до `0.31107091903686523` | ||
| Затраченное время также сократилось с `0.05` до `0.01` для `Array#include?` и `Set#include?` соотвественно. | ||
|
|
||
| ### Ваша находка №3 | ||
|
|
||
| Пятое место при профилировании с помощью `RubyProf::CPU_TIME` занимает преобразование `hash` в `json`. | ||
|
Owner
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. Можно было бы выиграть больше, начиная с первого! |
||
| Это можно оптимизировать с помощью замены библиотеки на более быструю. | ||
| До этого я уже сталкивался с такой проблемой и знаю, что самая быстрая библиотека это `Oj`. | ||
| Будем использовать ее и записывать сразу в файл результат `Oj.to_file` | ||
|
|
||
| В итоге скорость изменилась с | ||
|
|
||
| ``` shellsession | ||
| 18.05% 9.01% 0.26 0.13 0.00 0.13 1 JSON::Ext::Generator::GeneratorMethods::Hash#to_json | ||
| ``` | ||
|
|
||
| на | ||
|
|
||
| ``` shellsession | ||
| 0.55% 0.55% 0.01 0.01 0.00 0.00 1 <Module::Oj>#to_file | ||
| ``` | ||
|
|
||
| Общее время также уменьшилось с `1.449656` до `1.1936809999999998` | ||
|
|
||
| ## Результаты | ||
| В результате проделанной оптимизации наконец удалось обработать файл с данными. | ||
| Удалось улучшить метрику системы с *того, что у вас было в начале, до того, что получилось в конце* | ||
| Я также пробовал заменить `each` на `while`, но это давало грошевую оптимизацию и не приносило ощутимой разницы в метрике. | ||
|
|
||
| *Какими ещё результами можете поделиться* | ||
|
|
||
| ## Защита от регресса производительности | ||
| Для защиты от потери достигнутого прогресса при дальнейших изменениях программы сделано *то, что вы для этого сделали* | ||
| Для защиты от потери достигнутого прогресса при дальнейших изменениях программы был добавлен тест регрессии по времени | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| require './task-2.rb' | ||
| require 'benchmark/ips' | ||
| require 'minitest/autorun' | ||
|
|
||
| class Test < Minitest::Test | ||
| def setup | ||
| File.write('result.json', '') | ||
| end | ||
|
|
||
| def test_result | ||
| Benchmark.ips do |bench| | ||
| bench.report("Process 1 MB of data") do | ||
| work('data/data_1MB.txt') | ||
| end | ||
| end | ||
| end | ||
|
|
||
| def test_correctness | ||
| work('fixtures/data.txt') | ||
| expected_result = File.read('fixtures/expected_result.json') | ||
| assert_equal expected_result, File.read('result.json') | ||
| end | ||
|
|
||
| def test_time | ||
| result = Benchmark.realtime { work('data/data_1MB.txt') } | ||
| assert(result.round(2) < 0.2) | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| user,0,Leida,Cira,0 | ||
| session,0,0,Safari 29,87,2016-10-23 | ||
| session,0,1,Firefox 12,118,2017-02-27 | ||
| session,0,2,Internet Explorer 28,31,2017-03-28 | ||
| session,0,3,Internet Explorer 28,109,2016-09-15 | ||
| session,0,4,Safari 39,104,2017-09-27 | ||
| session,0,5,Internet Explorer 35,6,2016-09-01 | ||
| user,1,Palmer,Katrina,65 | ||
| session,1,0,Safari 17,12,2016-10-21 | ||
| session,1,1,Firefox 32,3,2016-12-20 | ||
| session,1,2,Chrome 6,59,2016-11-11 | ||
| session,1,3,Internet Explorer 10,28,2017-04-29 | ||
| session,1,4,Chrome 13,116,2016-12-28 | ||
| user,2,Gregory,Santos,86 | ||
| session,2,0,Chrome 35,6,2018-09-21 | ||
| session,2,1,Safari 49,85,2017-05-22 | ||
| session,2,2,Firefox 47,17,2018-02-02 | ||
| session,2,3,Chrome 20,84,2016-11-25 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| {"totalUsers":3,"uniqueBrowsersCount":14,"totalSessions":15,"allBrowsers":"CHROME 13,CHROME 20,CHROME 35,CHROME 6,FIREFOX 12,FIREFOX 32,FIREFOX 47,INTERNET EXPLORER 10,INTERNET EXPLORER 28,INTERNET EXPLORER 35,SAFARI 17,SAFARI 29,SAFARI 39,SAFARI 49","usersStats":{"Leida Cira":{"sessionsCount":6,"totalTime":"455 min.","longestSession":"118 min.","browsers":"FIREFOX 12, INTERNET EXPLORER 28, INTERNET EXPLORER 28, INTERNET EXPLORER 35, SAFARI 29, SAFARI 39","usedIE":true,"alwaysUsedChrome":false,"dates":["2017-09-27","2017-03-28","2017-02-27","2016-10-23","2016-09-15","2016-09-01"]},"Palmer Katrina":{"sessionsCount":5,"totalTime":"218 min.","longestSession":"116 min.","browsers":"CHROME 13, CHROME 6, FIREFOX 32, INTERNET EXPLORER 10, SAFARI 17","usedIE":true,"alwaysUsedChrome":false,"dates":["2017-04-29","2016-12-28","2016-12-20","2016-11-11","2016-10-21"]},"Gregory Santos":{"sessionsCount":4,"totalTime":"192 min.","longestSession":"85 min.","browsers":"CHROME 20, CHROME 35, FIREFOX 47, SAFARI 49","usedIE":false,"alwaysUsedChrome":false,"dates":["2018-09-21","2018-02-02","2017-05-22","2016-11-25"]}}} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| # frozen_string_literal: true | ||
|
|
||
| require 'zlib' | ||
|
|
||
| DATA = { | ||
| '1MB' => 1_048_576, | ||
| '4MB' => 4_194_304, | ||
| '10MB' => 10_485_760, | ||
| 'large' => nil | ||
| }.freeze | ||
|
|
||
| DIR = 'data'.freeze | ||
|
|
||
| def create_directory | ||
| Dir.mkdir(DIR) unless File.exist?(DIR) | ||
| end | ||
|
|
||
| def ungz_file | ||
| gz = Zlib::GzipReader.open('data_large.txt.gz') | ||
| unzipped = StringIO.new(gz.read) | ||
| gz.close | ||
| unzipped | ||
| end | ||
|
|
||
| def create_data(result = ungz_file) | ||
| DATA.each do |name, size| | ||
| size ||= result.size | ||
| result.pos = 0 | ||
| copied = 0 | ||
| target = File.open("#{DIR}/data_#{name}.txt", 'w+') | ||
| while (line = result.gets) | ||
| break if copied >= size | ||
|
|
||
| target.puts line | ||
| copied += line.size | ||
| end | ||
| target.close | ||
| end | ||
| end | ||
|
|
||
| create_directory | ||
| create_data |
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| require 'ruby-prof' | ||
| require './task-2.rb' | ||
|
|
||
| # RubyProf.measure_mode = RubyProf::WALL_TIME | ||
| RubyProf.measure_mode = RubyProf::CPU_TIME | ||
|
|
||
| result = RubyProf.profile do | ||
| work('data/data_1MB.txt', disable_gc: true) | ||
| end | ||
|
|
||
| # printer = RubyProf::FlatPrinter.new(result) | ||
| # printer.print(File.open('ruby_prof_flat.txt', 'w+')) | ||
|
|
||
| printer2 = RubyProf::GraphHtmlPrinter.new(result) | ||
| printer2.print(File.open('ruby_prof_graph.html', 'w+')) | ||
| # | ||
| # printer3 = RubyProf::CallStackPrinter.new(result) | ||
| # printer3.print(File.open('ruby_prof_callstack.html', 'w+')) | ||
|
|
||
| # printer4 = RubyProf::CallTreePrinter.new(result) | ||
| # printer4.print(:path => ".", :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.
👍