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

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

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

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

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

#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 и пропускаем экран приветствия.

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

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

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

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

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

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

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

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

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

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

На представлении функций становится понятно, что проблема производительности связана со строкой открытия файла. Она занимает 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 перед оптимизацией. Задача решена.

Вывод

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


Телеграм: Так себе программист