Шустрый, удобный и кроссплатформенный профилировщик C++ кода

Всем привет. Несколько месяцев назад мы вместе с victorzs решили сделать простой и удобный профилировщик c++ кода (подразумевается профилирование времени исполнения участков кода, функций).

8d641752b19246c19d96eb559ee63d05.png
Скриншот профилирования примера из SDK CryEngine

Существующие решения нам не подходили по ряду причин. Нам нужен был качественный профайлер, умеющий делать следующее:

  • Профилировать выбранные участки кода
  • Работать на нескольких платформах
  • Учитывать переключение контекста
  • Требовать минимальных дополнительных затрат памяти во время профилирования
  • Не накладывать дополнительных временных ограничений во время выполнения приложения. Согласитесь, если профилировщик будет работать дольше, чем профилиуремый косочек кода, то можно сделать некорректные выводы.

В результате тщательной проработки появился на свет профайлер, умеющий делать всё вышеперечисленное, и даже больше!
Если вы хотите знать, сколько времени работает ваш код, и иметь при этом объективные доказательства — прошу под кат, где я покажу, как использовать профилировщик.

Интегрирование в код


  1. Качаем и распаковываем свежий релиз отсюда: https://github.com/yse/easy_profiler/releases
  2. Прописываем компилятору директорию для поиска заголовочных файлов: /include
  3. Прописываем компоновщику директорию для поиска библиотек: /bin
  4. Добавляем definition компилятору: BUILD_WITH_EASY_PROFILER
  5. Добавляем блоки в те места кода, которые хотим замерить. Например:
    #include 
    
    void foo() {
        EASY_FUNCTION(profiler::colors::Magenta);// Начать блок с именем, совпадающим с именем функции
    
        EASY_BLOCK("Calculating sum");// Блок с цветом по умолчанию
        int sum = 0;
        for (int i = 0; i < 10; ++i) {
            EASY_BLOCK("Addition", profiler::colors::Red);// Блок будет закончен при выходе из области видимости
            sum += i;
        }
        EASY_END_BLOCK; // Закончить блок (в данном случае блок с именем "Calculating sum"
    
        EASY_BLOCK("Calculating multiplication", profiler::colors::Blue500);
        int mul = 1;
        for (int i = 1; i < 11; ++i)
            mul *= i;
        //на выходе из функции автоматически будут закрыты все открытые и незавершённые в этой функции блоки. В данном примере, автоматически закроются блоки с именами "Calculating multiplication" и "foo"
    }
    

  6. Не забываем положить рядом с собранным приложением библиотеку easy_profiler (*.dll или *.so). Или прописываем в системную переменную PATH (в линуксе достаточно в LD_LIBRARY_PATH) директорию /bin

Добавленные блоки в режиме сбора статистики занимают минимально-возможное время (как мы этого добились — в дальнейших статьях о технической реализации). На машине с процессором Core i7–5930K 3.5GHz, 16 Gb RAM, Win7 Pro в приложении с 12 потоками средняя «стоимость» одного блока — порядка 10–15 наносекунд! Подобный результат достигнут и на Fedora 22 . Вот график замеров (по оси x — количество блоков, по y — наносекунд на блок):

3e4afe8b77ac4ad3a6f8c805be4b7f13.png

Кроме того, видно, что зависимость линейная — количество блоков не влияет на временную характеристику.

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

Получение и анализ результатов происходит в программе с незамысловатым названием profiler_gui (в директории bin). Инициализация профилоровщика возможна двумя способами:

  1. Подключением по сокету приложением profiler_gui. Для этого необходимо инициализировать прослушивание сокета в профилируемом приложении. Это делается просто:
    profiler::startListen();
    

    Данная функция запускает поток, который слушает по порту 28077 (порт можно поменять параметром в функции profiler::startListen(portNumber)) команды управления. Остановить прослушивание можно вызовом функции (хотя это совсем не обязательно):
    profiler::stopListen();
    

    Сбор блоков начинается после коннекта profiler_gui к профилируемому приложению и нажатия на кнопку «Capture» на тул-баре. После остановки профилирования (нажать на «Stop») собранная информация передается через сокет из профилируемого приложения в profiler_gui и сразу же сохраняется на диск в файл easy_profiler.cache. Можно также сохранить всю информацию в отдельный файл (при этом просто происходит перемещение файла easy_profiler.cache).

  2. Сохранением результата в файл. Для этого сперва необходимо инициализировать профайлер, а затем в необходимый момент сохранить файл. Это делается следующим образом:
    int main()
    {
        EASY_PROFILER_ENABLE;
        /* do work*/
        profiler::dumpBlocksToFile("test_profile.prof");
    }
    

    После этого сохранённые файлы можно открыть в программе profiler_gui

Для получения информации о переключении контекста в Windows необходимо запускать профилируемое приложение с правами администратора. В linux дело обстоит чуть сложнее: необходимо запускать с привилегиями суперпользователя скрипт, находящийся в директории scripts/context_switch_logger.stp с параметрами. Данный скрипт интерпретируется программой systemtap. В Fedora нужно выполнить команду:
#stap -o /tmp/cs_profiling_info.log scripts/context_switch_logger.stp name APPLICATION_NAME

Где APPLICATION_NAME — имя профилируемого приложения, файл /tmp/cs_profiling_info.log — файл, куда записывается информация о переключениях контекста. Привилегии суперпользователя необходимы потому, что информацию о переключении контекста возможно получить только в пространстве ядра.

Анализ результатов


Для демонстрации возможностей анализатора результатов попрофилируем простой пример из CryEngine. В самом CryEngine есть несколько профилировщиков и для их организации существуют макросы, в которые легко встроить любой профайлер.

После компиляции запускаем тестовый пример, запускаем программу profiler_gui, коннектимся к приложению (иконка: 2168a1b58dec4d9ab80a31ad59e2c799.png, рядом с ней можно ввести ip-адрес или имя хоста, на котором запущено профилируемое приложение). После удачного коннекта (иконка немного позеленеет: ac37a24307a04f3486bd48b07181057d.png) можно запускать сессию профилирования. После нажатия на кнопку 13300c8ddf4d41df87f4b70144bdee71.png начнётся сбор статистики в профилируемом приложении. Для завершения сессии профилирования нужно закрыть появившееся окошко.

На скриншоте представлен общий вид программы с результатом

8d641752b19246c19d96eb559ee63d05.png

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

В центральной части представлена диаграмма времён либо потока, либо выбранного блока. Здесь время исполнения блока оценивается по вертикали, по горизонтали — время выполнения программы. т.е. можно смотреть всплески длительности блоков и при необходимости более детально оценить проблему.

В нижней части представлено дерево выполнения блоков для выбранного участка с подробнейшей статистикой. Здесь можно сортировать по длительности, искать самые долгие блоки, оценивать количество вызовов того или иного блока. Выбор участка осуществляется в верхней части экрана зажатием правой кнопки мыши и выделением необходимого куска.

Краткую статистику по блоку можно посмотреть в верхней части экрана. После наведения курсора на блок — появляется всплывающее окошко с краткой сводкой:
52cf47d287884effa093d4a933414a04.png

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

Ещё одной очень удобной фичей является динамическое включение/отключение блоков. Для этого надо открыть диалог (иконка a7708ce79ced4637a0caad1ef4554685.png) и в появившемся окне включить или отключить желаемые блоки. При следующей сессии профилирования эти настройки будут учтены.

59dd1834bb164574ad605f49128e3552.png
Отключаем сбор информации для функции C3DEngine::GetWaterLevel

Итак, преимущества профилировщика:
 — Скорость работы
 — Минимальные затраты памяти
 — Кроссплатформенность
 — Удобное и функционально графическое представление

Единственным ограничением использования является необходимость сборки профилируемого приложения компилятором, поддерживающим стандарт c++11.

Данный профилировщик будет полезен как для разработчиков движков игр (как ИИ, так и 3D), так и для тех, кто использует уже готовые движки, да и для всех, кто заботится о производительности своего приложения. Данный профайлер используется нами в рамках разработки системы визуализации для авиационных и тактических тренажёров.

Спасибо за внимание! С удовольствием ждём обратной связи (вопросы, пожелания, баги, звёздочки на гитхабе, пулл-реквесты). В процессе разработки были решены кое-какие нестандартные задачи, о чём хочется написать отдельные статьи.

Комментарии (3)

  • 2 февраля 2017 в 11:03

    +1

    Как то сумбурно изложено, как инициализировать профайлер в начале, я так понимаю вызовом EASY_PROFILER_ENABLE;?
    • 2 февраля 2017 в 11:07

      0

      Это макрос сразу начинает сессию профилирования. Это удобно для сохранения в дальнейшем в файл. Если подключаться через gui-приложение, то данный макрос необязателен
    • 2 февраля 2017 в 11:10

      +1

      Если требуется управлять запуском/остановкой сбора профилируемой информации из самого профилируемого приложения, то да, нужно вызывать EASY_PROFILER_ENABLE, EASY_PROFILER_DISABLE.
      Другой вариант — управлять запуском/остановкой из GUI. В этом случае, в профилируемом приложении нужно только вызвать profiler: startListen (), а команды старт/стоп будут приходить из GUI.

© Habrahabr.ru