Стоит отметить, что описанные инструменты не специфичны для Rust, а вполне применимы для широкого спектра языков. Но при этом я сразу оговорюсь, что речь будет идти о Linux инструментарии, что из этого портировано на другие ОС я не изучал.
Самый часто употребляемый способ профилирования - это ручной. Берём секундомер, замеряем сколько времени выполняется приложение и потом по наитию правим код. Если после этого скорость увеличилась - оставляем. Иногда конечно можно и так достичь результата, но к сожалению это редкость.
Следующий способ даём чуть больше информации - вставляем счётчики, таймеры в код и замеряем через них время выполнения интересующих кусков и потом анализируем. Такой способ конечно имеет право на жизнь, но есть большой шанс сделать это неправильно, плюс заниматься этим довольно трудозатратно, нет возможности вставить счётчики во все функции, а значит и получить общую информацию обо всём приложении.
Существуют инструменты, которые позволяют вставлять такие счётчики автоматически. Типичный представитель это gprof. На этапе компиляции в каждую функцию вставляется вызов профилировщика, а последний считает и потом в удобном виде показывает результаты работы. К сожалению для этого требуется поддержка компилятора, а в Rust я её обнаружить не смог. Отбрасываем.
Ещё один интересный подход демонстрирует valgrind, он по сути реализует виртуальную машину, внутри которой запускается исследуемая программа. Таким образом профилировщик отлавливает все выделения памяти, вызовы функций и т.п. собирая максимально полную информацию о работе приложения. Накопленные данные позволяют потом всесторонне анализировать время выполнения функций, ошибки выделения памяти, выход за границы массивов и т.п. Правда стоит отметить большой минус подхода - скорость работы приложения замедляется на пару порядков. Если будет время я попробую поиграться с ним и написать что получилось. А пока пропускаем.
И теперь о теме сегодняшнего разговора - о статических профилировщиках. Они работают существенно быстрее предыдущего способа, а так же ради них не нужно менять код, как в случае с gprof, правда и информации дают они не так что бы очень много, например утечки памяти с их помощью не найти. Работают они следующим образом: с заданной периодичностью, обычно довольно высокой (типично около 1000 раз в секунду) приложение останавливается и считываются различные его характеристики. В нашем случае это будут - текущая исполняемая функция и по возможности её call stack. Всё это сохраняется и приложение продолжает работу. Если "погонять" приложение в таком режиме достаточно долго, то с определённой долей уверенности, можно считать, что чем чаще "попадалась" нам функция, тем больше её реальное время исполнения. Самыми известными представителями являются oprofile и perf. С последним мы сегодня и будем играться.
Стоит отметить, что точность таких замеров является не 100%. Самые быстрые и редкие функции легко могут вообще не попасть в собранную статистику, но поскольку целью является выявление самых медленных и часто выполняемых, то отсечение всех остальных, нам даже на руку.
Приступим. Всяких разных способов анализа у perf много, подробно про все можно почитать в их wiki, я же буду собирать статистику через "perf record" и потом строить из неё красивые интерактивные графики через FlameGraph. Поэтому те кто хочет повторить у себя нечто подобное - установите perf и скачайте stackcollapse-perf.pl и flamegraph.pl из репозитория FlameGraph по ссылке выше. Ну и конечно же понадобится скомпилированное приложение на rust, я буду использовать свой rust-software-render.
От компилятора нам понадобится возможность добавить отладочные символы в код, а так же возможность компилировать с разными уровнями оптимизации. Для Rust это делается добавлением в cargo-конфиг следующих строк:
Про параметры для конфига можно почитать на сайте cargo. Тут выбран нулевой уровень оптимизации, их всего четыре: 0,1,2,3. На нулевом сохраняются все имена функций и разбирать результаты одно удовольствие, но при этом результаты очень далеки от тех, что будут в release сборке, а следовательно и узкие места в последней могут быть совсем иными. На 3-м уровне напротив от исходных функций мало что остаётся и понять там что-то становится сложно. Обычно рекомендуют выбирать 1-й, но ниже я покажу результаты для всех уровней.
После добавления этих строк, release сборка будет собираться с нужными нам параметрами. После компиляции, запускаем анализ вот так:
Тут флаг "-g" говорит о том, что мы хотим собирать граф вызовов, остальные оставим по умолчанию. Точность результатов будет зависеть от времени, на которое будет запущено приложение. Для моего случая хватало 1-2 минуты, потом разницу в результатах я особо не замечал. По завершению появится "perf.data" с результатами, которые можно проанализировать, запустив в той же директории в консоли команду:
Результат будет выглядеть как-то вот так:
В таком виде читать результаты довольно сложно, для построения красивого графика можно воспользоваться скриптами из FlameGraph, про которые я писал выше. Создать график можно вот так:
В результате мы получим svg файл, с динамическим содержанием, где можно посмотреть информацию по каждому блоку или рассмотреть его более детально. К сожалению вставить в блог его не получилось, поэтому я буду ниже вставлять лишь статическую картинку, а рядом прикладывать ссылку на исходный svg файл, который можно скачать и посмотреть в просмотрщике, который его поддерживает. Лично у меня локальные svg файлы хорошо открывает Firefox. Что бы было представление о возможностях я покажу gif с демонстрацией:
Для нулевого уровнял результат вот такой (оригинальный файл):
Для первого уровня результат будет выглядеть вот так, уже поближе к реальности (оригинальный файл):
Для второго, уже почти ничего не осталось от названий исходных функций (оригинальный файл):
А в режиме максимальной оптимизации всё выглядит вот так, в принципе картина по сравнению со вторым уровнем почти не поменялась (оригинальный файл):
Самый часто употребляемый способ профилирования - это ручной. Берём секундомер, замеряем сколько времени выполняется приложение и потом по наитию правим код. Если после этого скорость увеличилась - оставляем. Иногда конечно можно и так достичь результата, но к сожалению это редкость.
Следующий способ даём чуть больше информации - вставляем счётчики, таймеры в код и замеряем через них время выполнения интересующих кусков и потом анализируем. Такой способ конечно имеет право на жизнь, но есть большой шанс сделать это неправильно, плюс заниматься этим довольно трудозатратно, нет возможности вставить счётчики во все функции, а значит и получить общую информацию обо всём приложении.
Существуют инструменты, которые позволяют вставлять такие счётчики автоматически. Типичный представитель это gprof. На этапе компиляции в каждую функцию вставляется вызов профилировщика, а последний считает и потом в удобном виде показывает результаты работы. К сожалению для этого требуется поддержка компилятора, а в Rust я её обнаружить не смог. Отбрасываем.
Ещё один интересный подход демонстрирует valgrind, он по сути реализует виртуальную машину, внутри которой запускается исследуемая программа. Таким образом профилировщик отлавливает все выделения памяти, вызовы функций и т.п. собирая максимально полную информацию о работе приложения. Накопленные данные позволяют потом всесторонне анализировать время выполнения функций, ошибки выделения памяти, выход за границы массивов и т.п. Правда стоит отметить большой минус подхода - скорость работы приложения замедляется на пару порядков. Если будет время я попробую поиграться с ним и написать что получилось. А пока пропускаем.
И теперь о теме сегодняшнего разговора - о статических профилировщиках. Они работают существенно быстрее предыдущего способа, а так же ради них не нужно менять код, как в случае с gprof, правда и информации дают они не так что бы очень много, например утечки памяти с их помощью не найти. Работают они следующим образом: с заданной периодичностью, обычно довольно высокой (типично около 1000 раз в секунду) приложение останавливается и считываются различные его характеристики. В нашем случае это будут - текущая исполняемая функция и по возможности её call stack. Всё это сохраняется и приложение продолжает работу. Если "погонять" приложение в таком режиме достаточно долго, то с определённой долей уверенности, можно считать, что чем чаще "попадалась" нам функция, тем больше её реальное время исполнения. Самыми известными представителями являются oprofile и perf. С последним мы сегодня и будем играться.
Стоит отметить, что точность таких замеров является не 100%. Самые быстрые и редкие функции легко могут вообще не попасть в собранную статистику, но поскольку целью является выявление самых медленных и часто выполняемых, то отсечение всех остальных, нам даже на руку.
Приступим. Всяких разных способов анализа у perf много, подробно про все можно почитать в их wiki, я же буду собирать статистику через "perf record" и потом строить из неё красивые интерактивные графики через FlameGraph. Поэтому те кто хочет повторить у себя нечто подобное - установите perf и скачайте stackcollapse-perf.pl и flamegraph.pl из репозитория FlameGraph по ссылке выше. Ну и конечно же понадобится скомпилированное приложение на rust, я буду использовать свой rust-software-render.
От компилятора нам понадобится возможность добавить отладочные символы в код, а так же возможность компилировать с разными уровнями оптимизации. Для Rust это делается добавлением в cargo-конфиг следующих строк:
[profile.release] opt-level = 0 debug = true
После добавления этих строк, release сборка будет собираться с нужными нам параметрами. После компиляции, запускаем анализ вот так:
perf record -g ./rust-software-render
perf report
В таком виде читать результаты довольно сложно, для построения красивого графика можно воспользоваться скриптами из FlameGraph, про которые я писал выше. Создать график можно вот так:
perf script | ./stackcollapse-perf.pl | ./flamegraph.pl > report_lvl_1.svg
Для нулевого уровнял результат вот такой (оригинальный файл):
Для первого уровня результат будет выглядеть вот так, уже поближе к реальности (оригинальный файл):
Для второго, уже почти ничего не осталось от названий исходных функций (оригинальный файл):
А в режиме максимальной оптимизации всё выглядит вот так, в принципе картина по сравнению со вторым уровнем почти не поменялась (оригинальный файл):
Клевая статья!
ОтветитьУдалитьА kcachegrind или что подобное нельзя использовать? Я хоть и люблю все простое и консольное, но для такого полноценный gui должен быть полезен)
Эх, вот бы еще был какой-нибудь более-менее стандартный "покадровый" профилировщик - для игр-то от обычного профилировщика толку не так и много.
> информации дают они не так что бы очень много, например утечки памяти с их помощью не найти
УдалитьНу это же не задача профилировщика) Тут скорее вопрос в большой погрешности результатов.
Еще, конечно, не слишком удобны имена функций в отладочной информации. Это и при простой отладке кода на ржавчине часто раздражает.
УдалитьИнтересно, можно ли с этим чего-то сделать?
Kcachegrind как я понял только с Valgrind работает, а для perf единственный юзабельный с позволения сказать "gui" это вот эти графики, что я привел. По крайней мере ничего лучше я не нашел.
Удалить>>Ну это же не задача профилировщика) Тут скорее вопрос в большой погрешности результатов.
Я понимаю, просто упомянул, что Valgrind с его подходом - это умеет делать
>>Эх, вот бы еще был какой-нибудь более-менее стандартный "покадровый" профилировщик - для игр-то от обычного профилировщика толку не так и много.
Ну там тормозит обычно графика, так для нее есть всякие http://developer.amd.com/tools-and-sdks/graphics-development/gpu-perfstudio/ и им подобные, правда большинство под винду.
Валгринд, если ничего не путаю, изначально и был средством поиска ошибок работы с памятью. Профайлер потом припилили) Ну да не важно, интерактивные приложения под валгриндом гонять - в любом случае издевательство)) Хотя я даже свой zoc так мучал как-то, искал почему текстуры долго грузятся - ничего, всего раз в 10 медленнее работало. Просто нужно терпение)
УдалитьА в играх часто тормозит не только графика. особенно когда она в процессе разработки - и отрисовка окошечек может все время кадра пожрать, и физика, и хитрая игровая логика и что угодно. Видимо, придется вручную секции расставлять (как тут https://github.com/cmr/hprof) и довольствоваться текстовым выводом)
Не могу только понять зачем нужен именно "покадровый" профилировщик? Если после 10 минут работы приложения видно, что тормозит некая функция "foo", разве это автоматически не значит, что в каждом кадре в среднем тормозит тоже она же?
УдалитьДля большинства проблем нужно, как минимум, разделить работу программы на инициализацию-загрузку ресурсов, и на нормальную работу главного цикла. Ну разные игровые экраны тоже могут картину испортить - пока ты там из главного меню "дотянешься" до непосредственно игрового процесса.
УдалитьЗатем, многие вещи, конечно, можно поймать в "усредненном" по всем кадрам графике, но совсем не факт, что эта тормозящаяя `foo` вызывается в каждом кадре. Особенно если с игровой логикой связано - тут штуки типа юнитевого покадрового профайлера бывают дико полезны для ловли и анализа таких "пиков" - http://www.gamasutra.com/db_area/images/blog/203842/Unity_CPU_profiler.png .
скорее даже вот для подобного: http://i.imgur.com/QyDkjy3.png
УдалитьДа действительно, не подумал об этом. Сильно привык, что у меня каждый кадр делается по сути одно и тоже.
Удалить