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

в 7:49, , рубрики: c++, performance tunning, ещё один профилировщик, производительность, профилирование, разработка игр

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

Шустрый, удобный и кроссплатформенный профилировщик C++ кода - 1
Скриншот профилирования примера из SDK CryEngine

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

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

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

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

  1. Качаем и распаковываем свежий релиз отсюда: https://github.com/yse/easy_profiler/releases
  2. Прописываем компилятору директорию для поиска заголовочных файлов: <easy_profiler_release_dir>/include
  3. Прописываем компоновщику директорию для поиска библиотек: <easy_profiler_release_dir>/bin
  4. Добавляем definition компилятору: BUILD_WITH_EASY_PROFILER
  5. Добавляем блоки в те места кода, которые хотим замерить. Например:
    #include <easy/profiler.h>
    
    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) директорию <easy_profiler_release_dir>/bin

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

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

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

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

Получение и анализ результатов происходит в программе с незамысловатым названием 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, коннектимся к приложению (иконка: Шустрый, удобный и кроссплатформенный профилировщик C++ кода - 3, рядом с ней можно ввести ip-адрес или имя хоста, на котором запущено профилируемое приложение). После удачного коннекта (иконка немного позеленеет: Шустрый, удобный и кроссплатформенный профилировщик C++ кода - 4) можно запускать сессию профилирования. После нажатия на кнопку Шустрый, удобный и кроссплатформенный профилировщик C++ кода - 5 начнётся сбор статистики в профилируемом приложении. Для завершения сессии профилирования нужно закрыть появившееся окошко.

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

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

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

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

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

Краткую статистику по блоку можно посмотреть в верхней части экрана. После наведения курсора на блок — появляется всплывающее окошко с краткой сводкой:
Шустрый, удобный и кроссплатформенный профилировщик C++ кода - 7

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

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

Шустрый, удобный и кроссплатформенный профилировщик C++ кода - 9
Отключаем сбор информации для функции C3DEngine::GetWaterLevel

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

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

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

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

Автор: yse

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js