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


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

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

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

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

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

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


  1. Качаем и распаковываем свежий релиз отсюда: https://github.com/yse/easy_profiler/releases
  2. Если вы испольузете CMake для сборки, то
    • Определите переменную CMAKE_PREFIX_PATH, которая указывает на директорию <easy_profiler_release_dir>/cmake/easy_profiler из релиза
    • Используйте find_package(easy_profiler REQUIRED) и target_link_libraries(... easy_profiler)

    В ином случае:
    • Прописываем компилятору директорию для поиска заголовочных файлов: <easy_profiler_release_dir>/include
    • Прописываем компоновщику директорию для поиска библиотек: <easy_profiler_release_dir>/bin

  3. Добавляем definition компилятору: BUILD_WITH_EASY_PROFILER
  4. Добавляем блоки в те места кода, которые хотим замерить. Например:

    #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"
    }
    

  5. Не забываем положить рядом с собранным приложением библиотеку 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 — наносекунд на блок):



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

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


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

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



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

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

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

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


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

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


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

Итак, преимущества профилировщика:

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

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

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

Лицензия либо Apache 2.0, либо GPL v.3 — как на либину, так и на gui. Используйте любую из этих лицензий.

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

Поделиться с друзьями
-->

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


  1. AndreySu
    02.02.2017 11:03
    +1

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


    1. yse
      02.02.2017 11:07

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


    1. victorzs
      02.02.2017 11:10
      +1

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


      1. AndreySu
        02.02.2017 11:18

        Возможно ли рисовать в риалтайме графики профилирования, как, например, это сделано в профилировщике Visual Studio? В GUI.


        1. yse
          02.02.2017 11:31

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


  1. lookid
    02.02.2017 11:32

    EASY_BLOCK("Calculating multiplication", profiler::colors::Blue500);

    Ох длиннющая же запись. Предеться обкладывать дефайнами или инлайнами. Пишешь код в 30 символов, а профайлер в 130.


    1. victorzs
      02.02.2017 12:57
      +2

      Цвет задавать не обязательно (так просто нагляднее), а для неймспейсов можно завести более короткие алиасы (на крайний случай using namespace).
      Цвета имеют формат uint32_t (ARGB) и использование profiler::colors никому не навызявается — вы можете сформировать свой набор цветов или задавать цвет напрямую (например, 0xff2196f3 вместо profiler::colors::Blue500).
      Ну а длинное или короткое имя задать блоку — это ваш выбор.


  1. iSage
    02.02.2017 13:08
    +2

    1. >Кроссплатформенный
    Я, конечно, собирать не пробовал, но в описании только lib/win. Мака нет.
    2. Что с лицензией? Что под апаче, что под gpl?
    3. Минусы: требует правки кода, умеет только именованные участки, не умеет показывать узкие места ( в отличии от perf, xcode и того, что там в студии)


    1. yse
      02.02.2017 13:14

      Мака нет.

      К сожалению под рукой мака нет, но clang`ом вроде собирается, используется Qt. Так что судя по всему под маком тоже взлетит без проблем. Если есть у кого собрать и проверить под маком — будем рады
      Что с лицензией?

      Используйте либо апач, либо GPL. И gui и либина под этими лицензиями. Обновлю информацию сразу в статью. Спасибо!
      Минусы: требует правки кода...

      Я бы не стал говорить, что это прям минус, неудобно, да, но практика показывает, что иногда наоборот кусочно полезно профилировать.


    1. BasilioCat
      02.02.2017 15:06

      Мне кажется, что на Mac/FreeBSD/Solaris лучше использовать DTrace


      1. iSage
        02.02.2017 15:12

        У мака гуевина в xcode удобная, afaik там dtrace под капотом.


    1. victorzs
      02.02.2017 16:53
      +1

      По 3 пункту: я могу ошибаться, но вроде бы профайлеры, которые собирают информацию "на лету" позволят вам увидеть информацию по приложению в целом, а не по каждому фрейму в отдельности? И это безусловно полезно и во многих случаях большего и не нужно.
      Наш профайлер позволит вам увидеть что происходило в каждом конкретном фрейме. Это может быть полезно, если нужно обнаружить причину тормозов в конкретный момент времени.
      Для полноты картины я бы использовал оба типа профайлеров.


      1. victorzs
        02.02.2017 17:41
        +3

        Прошу прощения, я действительно ошибся. Вспомнил, что в том же VTune можно выделить интересующий участок на графике.


      1. mkarev
        02.02.2017 21:14
        +2

        Для полноты картины я бы использовал оба типа профайлеров.

        У нас как раз такая практика — "настоящим" профайлером ищем узкие места, а потом обкладываем их в коде замерами, подобными сабжевым, и вперед оптимизировать.
        Получаем преимущества обоих методов


        • точная локализация узких мест инструментальным профилированием (xcode, vtune, etc)
        • быстрые и повторяемые замеры ручным профилированием во время решения найденных проблем


  1. Alexey2005
    02.02.2017 16:14
    +2

    Лучший из известных мне профилировщиков — Intel VTune. Профилировщик от AMD (CodeAnalyst) уже значительно хуже, зато бесплатный. Оба они работают и в Windows, и в Linux. Все остальные известные мне профилировщики по возможностям вообще не сравнимы с этими двумя.


    1. mkarev
      02.02.2017 21:02

      XCode Instruments ИМХО вполне сравним с V-Tune'ом, бесплатно, без SMS


    1. lieff
      02.02.2017 21:56

      А самое главное — VTune один из немногих использует хардварные возможности. У интеля есть либа с исходниками на гитхабе, дающая доступ к этим хардварным перформанс счетчикам, это самый точный способ на интеле. По идее эти хардварные каунтеры можно прикрутить и к этому профилировщику, добавить sampling профайл и не надо будет менять код вообще, нужна только отладочная инфа. Только под виндой ему нужен драйвер для доступа к MSR регистрам, потому в винде в том числе и VTune иногда ломается, в лине работает из коробки без драйвера.


      1. mkarev
        03.02.2017 07:03

        XCode Instruments умеет считать HW счетчики производительности.
        Можно выбрать из большого списка, что интересует, например кэш промахи по данным L1.
        Правда кол-во одновременно наблюдаемых счетчиков ограничено, но я думаю это ограничение конкретного CPU.
        Правда на ARM, к сожалению, performance counters не доступны, но с другой стороны v-tune эту архитектуру вообще не поддерживает.


        XCode Instruments не зависит от исходного кода, ему нужен бинарник и файл с символами.


        1. lieff
          03.02.2017 10:51

          Да, XCode тоже поддерживает, в либе поддержка macos кстати тоже есть, и тоже нужен MacMSRDriver.
          Так что теоретически можно сделать аля кроссплатформенный VTune отпенсорсный.


      1. 0xd34df00d
        03.02.2017 19:19

        в лине работает из коробки без драйвера.

        Нет, без драйвера работает только basic hotspots (по крайней мере, из того, что я пробовал). Для всяких там advanced hotspots, general exploration и уж тем более архитектурно-специфичных анализов также нужны модули ядра.


        1. lieff
          03.02.2017 21:17

          Я про либу с гитхаба, как и perf отдельные модули ядра им не нужны, все уже есть в ядре давным давно (конфиг CONFIG_HW_PERF_EVENTS, отключенным его не видел). Ими можно пользоваться и вообще без софта из скриптов через /sys/, но не все возможности, а так же в своей программе через libperf.


  1. elderorb
    02.02.2017 19:33
    +4

    Раз уж на Qt, как насчёт плагина для QtCreator-а? :)


    1. yse
      03.02.2017 14:01
      +1

      Вы имеете в виду gui проинтегрировать в QtCreator? Любопытная мысль, что инструмент под рукой.
      Были идеи плагина, который позволял бы автоматически блоки проставлять в функции, отображал бы цвет блоков, в общем плагин для кода. А вот гуи добавить что-то мыслей не возникло =( Спасибо за подсказку!


      1. lookid
        03.02.2017 14:47
        +1

        Можно сделать плагин, который ставит на полях метки для профилеровщика. Чтобы руками не писать. Или в контектное меню добавить.


        1. victorzs
          08.02.2017 13:22

          Мы, к сожалению, опыта написания плагинов для QtCreator-а не имеем. Если есть желание, можете включиться в разработку и создать плагин. У Вас, скорее всего, это получится лучше, а мы, если потребуется, проконсультируем по особенностям работы профайлера / гуи :)


  1. oYASo
    03.02.2017 04:48
    +1

    Выглядит клево! Попробуем, спасибо!


    1. yse
      03.02.2017 14:11

      Спасибо! Ждём обратной связи! =)


      1. oYASo
        03.02.2017 17:37
        +1

        О чем хочется сразу сказать — поправьте нормально файлы сборки. Дело в том, что, во-первых, при указании cmake папки сборки, финальные исполняемые файлы все равно зачем-то копируются в каталог сорцов. Портить папку с сорцами, особенно когда я указал этого не делать — плохой тон.
        Во-вторых, хочется все это проинсталлировать в систему (Linux), а не собирать все это в отдельную папку, потом прописать пути и т.д. Инсталляции пока нет.
        В-третьих, раз уж используется cmake, хочется в своем проекте просто написать find_package(easy_profiler) и получить сразу пути к хедерам и библиотекам, а также все необходимые дефайны. А чтобы руками не писать модуль FindEasyProfiler.cmake, рекомендую почитать про package layout cmake. То есть это позволит проинсталлировать в систему вашу библиотеку сразу с автоматически сгенерированным конфигом cmake со всеми правильными путями, клиенту нужно будет написать в своем коде только find_package(easy_profiler).

        Есть еще вопросы конкретно по использованию, но я их в личку напишу.


        1. victorzs
          03.02.2017 18:42
          +1

          Спасибо за ценные замечания! Думаю, в ближайшие дни сможем уделить время системе сборки.
          Вопросы по использованию можете также дублировать мне.


  1. yrouban
    04.02.2017 15:21
    +1

    Молодцы, хороший прибор сделали. Насколько я понимаю, GUI часть намного сложнее чем сборщик :)
    Вдохновение пришло, наверное, от Event Tracing for Windows API + GPUView.
    Есть ещё аналогичный подход у Гугла — systrace (https://developer.android.com/studio/profile/systrace.html)
    Intel PCM, про который тут упоминали, просто меряет hardware performace counters без привязки к коду. Это совсем другой подход. Раз уж тут упомянули Vtune и CodeXL, то было бы очень круто сделать из вашего профилировщика плагин для CodeXL. Ещё крутой фичей было бы профилирование чужого кода (то есть без возможности перекомпиляции), например замерять время вызова некоторых функций или участков кода. Это можно сделать с помощью инструментирования бинарного кода (см. pintool).
    Я с вами согласен в том, что часто бывает проще сделать свой прибор на известной технологии, чем изучать чужие приборы и подходы и адаптировать их под свои проекты.


    1. victorzs
      09.02.2017 14:08

      Спасибо :)
      Насчет GUI vs core — даже не знаю как оценить что сложнее, и там и там есть свои хитрые особенности.
      А вдохновлялись скорее проектом Brofiler :) см. комментарий ниже по тексту.

      По поводу профилирования без встраивания в код — пока особо не разбирались в этом вопросе (и в кросс-платформенном варианте здесь, я думаю, все очень непросто), решили начать с простого.


  1. yse
    07.02.2017 19:02

    Уважаемые хабравчане, огромное спасибо за конструктивную обратную связь. Это действительно мотивирует!

    Отдельное спасибо oYASo за совет об автоматической генерации файлов cmake (посредством package layout cmake). Пользователям данной системы сборки теперь достаточно писать find_package(easy_profiler). Это действительно удобно. Обновил информацию в статье.


    1. oYASo
      08.02.2017 01:35
      +2

      Сейчас хорошо, но я все-таки предлагаю довести до отлично :)

      Дело в том, что в коде сборки вы переопределяете CMAKE_INSTALL_PREFIX на папку в директории с сорцами sdk. Если ее оставить пустой, то по умолчанию на никсах она определена как /usr/local. Вам остается просто написать, что инсталлировать хедеры нужно в include/easy_profiler, либки в lib/, исполняемые файлы в bin/, файлы со скриптами можно кинуть в share/. А фишка в том, что cmake по умолчанию ищет свои конфигурационные файлы в /usr/local/lib/cmake. Это значит, что если вы просто на никсах будете устанавливать все в папки по умолчанию, то прям из коробки можно будет писать
      cmake…
      make
      sudo make install

      и дальше в коде, без CMAKE_PREFIX_PATH, сразу писать
      find_package(easy_profiler).

      На винде, увы, это не прокатит, потому что там нет логики рассовывания библиотек по системе, поэтому CMAKE_PREFIX_PATH писать придется.

      Далее, есть еще такое предложение. Везде в коде вы пишите add_definitions и include_directories. На самом деле, это штука устаревшая, и работает локально. Намного удобнее использовать target_compile_definitions и target_include_directories, которым можно указывать дополнительно PUBLIC, PRIVATE, INTERFACE в зависимости от того, нужно ли экспортировать эти директивы и хедеры в проекты, которые подключают ваш проект. В итоге, если вы определите target_compile_definitions(easy_profiler PUBLIC BUILD_WITH_EASY_PROFILER), то все проекты, которые будут указывать в target_link_libraries вашу библиотеку, автоматически подключает все определения с модификатором PUBLIC. Тоже самое касается и хедеров, если проделать аналогичное с target_include_directories.


      1. victorzs
        08.02.2017 08:34

        Да, давненько мы в cmake не заглядывали. Вот так пользуешься старьем, оно работает и не знаешь про такие полезные конструкции :)
        Спасибо :)


  1. Darkxiv
    09.02.2017 13:39

    Странно, что ещё никто не упомянул brofiler. Вы случайно не им вдохновлялись?


    1. victorzs
      09.02.2017 13:51
      +1

      Вы совершенно правы :)
      Однако он Windows-only, а нам нужно было профилировать и на Linux. Да, у него есть pull-request с портом под Linux, но он довольно старый и так и не влит. Ну и плюс некоторые вещи для себя более удобно сделали.