Профилирование 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 перед оптимизацией. Задача решена.
Вывод
Если есть необходимость в точных измерениях времени выполнения функций, а также подсчета количества их вызовов, то без способа с инструментированием не обойтись. В остальных случаях подойдет рассмотренный метод.
Телеграм: Так себе программист