понедельник, 16 ноября 2015 г.

Профилирование в Rust

Стоит отметить, что описанные инструменты не специфичны для 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-конфиг следующих строк:
[profile.release]
opt-level = 0
debug = true
Про параметры для конфига можно почитать на сайте cargo. Тут выбран нулевой уровень оптимизации, их всего четыре: 0,1,2,3. На нулевом сохраняются все имена функций и разбирать результаты одно удовольствие, но при этом результаты очень далеки от тех, что будут в release сборке, а следовательно и узкие места в последней могут быть совсем иными. На 3-м уровне напротив от исходных функций мало что остаётся и понять там что-то становится сложно. Обычно рекомендуют выбирать 1-й, но ниже я покажу результаты для всех уровней.

После добавления этих строк, release сборка будет собираться с нужными нам параметрами. После компиляции, запускаем анализ вот так:
perf record -g ./rust-software-render
Тут флаг "-g" говорит о том, что мы хотим собирать граф вызовов, остальные оставим по умолчанию. Точность результатов будет зависеть от времени, на которое будет запущено приложение. Для моего случая хватало 1-2 минуты, потом разницу в результатах я особо не замечал. По завершению появится "perf.data" с результатами, которые можно проанализировать, запустив в той же директории в консоли команду:
perf report
Результат будет выглядеть как-то вот так:
В таком виде читать результаты довольно сложно, для построения красивого графика можно воспользоваться скриптами из FlameGraph, про которые я писал выше. Создать график можно вот так:
perf script | ./stackcollapse-perf.pl | ./flamegraph.pl > report_lvl_1.svg
В результате мы получим svg файл, с динамическим содержанием, где можно посмотреть информацию по каждому блоку или рассмотреть его более детально. К сожалению вставить в блог его не получилось, поэтому я буду ниже вставлять лишь статическую картинку, а рядом прикладывать ссылку на исходный svg файл, который можно скачать и посмотреть в просмотрщике, который его поддерживает. Лично у меня локальные svg файлы хорошо открывает Firefox. Что бы было представление о возможностях я покажу gif с демонстрацией:


Для нулевого уровнял результат вот такой (оригинальный файл):
Для первого уровня результат будет выглядеть вот так, уже поближе к реальности (оригинальный файл):

Для второго, уже почти ничего не осталось от названий исходных функций (оригинальный файл):

А в режиме максимальной оптимизации всё выглядит вот так, в принципе картина по сравнению со вторым уровнем почти не поменялась (оригинальный файл):

9 комментариев:

  1. Клевая статья!

    А kcachegrind или что подобное нельзя использовать? Я хоть и люблю все простое и консольное, но для такого полноценный gui должен быть полезен)

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

    ОтветитьУдалить
    Ответы
    1. > информации дают они не так что бы очень много, например утечки памяти с их помощью не найти

      Ну это же не задача профилировщика) Тут скорее вопрос в большой погрешности результатов.

      Удалить
    2. Еще, конечно, не слишком удобны имена функций в отладочной информации. Это и при простой отладке кода на ржавчине часто раздражает.

      Интересно, можно ли с этим чего-то сделать?

      Удалить
    3. Kcachegrind как я понял только с Valgrind работает, а для perf единственный юзабельный с позволения сказать "gui" это вот эти графики, что я привел. По крайней мере ничего лучше я не нашел.

      >>Ну это же не задача профилировщика) Тут скорее вопрос в большой погрешности результатов.
      Я понимаю, просто упомянул, что Valgrind с его подходом - это умеет делать

      >>Эх, вот бы еще был какой-нибудь более-менее стандартный "покадровый" профилировщик - для игр-то от обычного профилировщика толку не так и много.
      Ну там тормозит обычно графика, так для нее есть всякие http://developer.amd.com/tools-and-sdks/graphics-development/gpu-perfstudio/ и им подобные, правда большинство под винду.

      Удалить
    4. Валгринд, если ничего не путаю, изначально и был средством поиска ошибок работы с памятью. Профайлер потом припилили) Ну да не важно, интерактивные приложения под валгриндом гонять - в любом случае издевательство)) Хотя я даже свой zoc так мучал как-то, искал почему текстуры долго грузятся - ничего, всего раз в 10 медленнее работало. Просто нужно терпение)

      А в играх часто тормозит не только графика. особенно когда она в процессе разработки - и отрисовка окошечек может все время кадра пожрать, и физика, и хитрая игровая логика и что угодно. Видимо, придется вручную секции расставлять (как тут https://github.com/cmr/hprof) и довольствоваться текстовым выводом)

      Удалить
    5. Не могу только понять зачем нужен именно "покадровый" профилировщик? Если после 10 минут работы приложения видно, что тормозит некая функция "foo", разве это автоматически не значит, что в каждом кадре в среднем тормозит тоже она же?

      Удалить
    6. Для большинства проблем нужно, как минимум, разделить работу программы на инициализацию-загрузку ресурсов, и на нормальную работу главного цикла. Ну разные игровые экраны тоже могут картину испортить - пока ты там из главного меню "дотянешься" до непосредственно игрового процесса.

      Затем, многие вещи, конечно, можно поймать в "усредненном" по всем кадрам графике, но совсем не факт, что эта тормозящаяя `foo` вызывается в каждом кадре. Особенно если с игровой логикой связано - тут штуки типа юнитевого покадрового профайлера бывают дико полезны для ловли и анализа таких "пиков" - http://www.gamasutra.com/db_area/images/blog/203842/Unity_CPU_profiler.png .

      Удалить
    7. скорее даже вот для подобного: http://i.imgur.com/QyDkjy3.png

      Удалить
    8. Да действительно, не подумал об этом. Сильно привык, что у меня каждый кадр делается по сути одно и тоже.

      Удалить