Профилирование C++ кода в Visual Studio без инструментирования

Разбираемся, как решать проблемы производительности программы с помощью профилировщика Visual Studio без инструментирования

3 мин на чтение

Исходные данные

Рассмотрим код с проблемой производительности - он выполняется слишком долго. Проблема может показаться очевидной. Да что там показаться, так оно и есть. Но представим, что мы не знаем, где проблема.

#include <iostream>
#include <fstream>

using namespace std;

void WriteFile1(int n)
{
  for (int i = 0; i < n; ++i)
  {
    fstream file("file.txt");
    if (file)
      file << i << " ";
  }
}

void WriteFile2(int n)
{
  for (int i = 0; i < n; ++i)
  {
    fstream file("file.txt");
    if (file)
      file << i << " ";
  }
}

int main()
{
  cout << "WriteFile small" << endl;
  WriteFile1(100000);
  cout << "WriteFile big" << endl;
  WriteFile2(1000000);
  return 0;
}

Можно попытаться решить задачу без специальных инструментов, измерив время выполнения участков кода “вручную”. Для этого фиксируется время начала и конца выполнения участка кода и разница между ними выводится на экран.

Но мы не хотим писать лишний код, поэтому используем профилировщик Visual Studio. Профилирование в Visual Studio можно выполнять с инструментированием и без. Под инструментированием подразумевается встраивание в исходный код специальных меток во время компиляции. По этим меткам профилировщик будет делать измерения. Главный недостаток здесь - нужна специальная сборка. Плюс программа становится громоздкой и немного “тормознутой”.

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

Первый запуск профилировщика

Visual Studio позволяет профилировать приложения даже если они не разрабатываются в этой среде. Пусть это будет наш случай.

Запускаем Visual Studio и пропускаем экран приветствия.

Стартовый экран Visual Studio 2022 со ссылкой "Продолжить без кода"

Переходим к запуску профилировщика производительности.

Запуск профилировщика производительности через меню Отладка → Окна в Visual Studio

В качестве целевого объекта выбираем “Исполняемый файл”.

Выбор целевого объекта анализа "Исполняемый файл" в профилировщике Visual Studio

Настраиваем параметры запуска исполняемого файла. Как минимум указываем путь к файлу. Рабочая директория пусть совпадает с директорией исполняемого файла.

Настройка пути к исполняемому файлу и рабочей папки для запуска профилировщика

Теперь остается выбрать режим профилирования и можно запускать. “Использование ЦП” - наш случай. Профилирование без инструментирования.

Выбор режима "Использование ЦП" и запуск профилирования без инструментирования

После нажатия на “Начать” будет запущена целевая программа под профилировщиком. По окончании работы программы профилировщик построит отчет.

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

Отчет профилировщика с таблицей критического пути вызовов функции WriteFile2

Таблица критического пути содержит два параметра:

  • Общее время ЦП - это время, затраченное функцией вместе с ее внутренними вызовами других функций. (ЦП - центральный процессор).

  • Собственное время ЦП - это время, затраченное функцией, исключая ее внутренние вызовы других функций.

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

Представление функции WriteFile2 с указанием строки открытия файла, занимающей 84,48% времени выполнения

На представлении функций становится понятно, что проблема производительности связана со строкой открытия файла. Она занимает 84,48% всего времени выполнения программы. Предположим, что это связано с тем, что открытие файла выполняется на каждой итерации цикла. Слишком часто…

Второй запуск профилировщика

Попробуем вынести код открытия файла за пределы цикла и снова запустим профилирование, чтобы убедиться в правильности решения.

#include <iostream>
#include <fstream>

using namespace std;

void WriteFile1(int n)
{
  fstream file("file.txt");
  if (file)
    for (int i = 0; i < n; ++i)
      file << i << " ";
}

void WriteFile2(int n)
{
  fstream file("file.txt");
  if (file)
    for (int i = 0; i < n; ++i)
      file << i << " ";
}

int main()
{
  cout << "WriteFile small" << endl;
  WriteFile1(100000);
  cout << "WriteFile big" << endl;
  WriteFile2(1000000);
  return 0;
}

Повторное профилирование показало… Ничего не показало. Программа выполнилась настолько быстро, что профилировщик не зафиксировал время выполнения вложенных функций. Общее время ЦП стало равным 3 против 6159 перед оптимизацией. Задача решена.

Отчет повторного профилирования после оптимизации: общее время ЦП снизилось с 6159 до 3 единиц

Вывод

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


Обсудить в Телеграм


Свежие записи

Чему учиться в эпоху ИИ?

Чему учиться в эпоху ИИ?

Заметка по книге Букреева и ДеУолта “Эверест. Смертельное восхождение”

1 мин на чтение

Doc As Code: диаграммы PlantUML в Doxygen

Doc As Code: диаграммы PlantUML в Doxygen

Включаем поддержку UML-диаграмм в Doxygen и размышляем о последствиях

3 мин на чтение

Какие файлы нельзя коммитить в Git

Какие файлы нельзя коммитить в Git

Разбираемся, как не превратить репозиторий Git в Google-диск

3 мин на чтение

Зачем нужны UML диаграммы?

Зачем нужны UML диаграммы?

Выясняем, когда и для чего разработчики используют UML

3 мин на чтение

Как разбить проект CMake на подпроекты

Как разбить проект CMake на подпроекты

Разделяем на подпроекты существующий проект C++ на CMake

1 мин на чтение