О поиске утечек памяти в С++/Qt приложениях

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Каждый программист, работающий с языком С++, должен уметь находить утечки памяти. Язык С++ - сложный язык, делать ошибки легко, а находить их бывает муторно. Особенно это касается утечек памяти. Ситуация с отловом утечек памяти только усугубляется, если в коде С++ используется библиотека Qt.

Эта статья посвящена разным инструментам, которые можно с той или иной степенью успешности применять для отлова утечек памяти в С++/Qt приложениях (desktop). Инструменты будут рассмотрены в связке с IDE Visual Studio 2019. В статье будут рассмотрены не все возможные инструменты, а лишь наиболее популярные и эффективные.

Наша команда давно и пристально изучает подобные инструменты и использует их в своей работе. Объем кода, на котором есть возможность проверить подобные инструменты, составляет около 1.5 миллиона строк. Опираясь на большой практический опыт, мы расскажем о плюсах и минусах разных инструментов, расскажем, что они способны найти, а что не по зубам, расскажем о неочевидных нюансах и, главное, составим сводную сравнительную таблицу по реальному примеру. Мы постараемся максимально быстро и просто ввести в курс дела (показать быстрый старт), потому даже если ты, читатель, никогда не занимался поиском утечек памяти, эта статья поможет за пару часов разобраться и найти свою первую утечку. Поехали!

В чем проблема?

Утечка памяти – ситуация, когда память была выделена (например, оператором new) и ошибочно не была удалена соответствующим оператором/функцией удаления (например, delete).

Пример 1.

int* array = nullptr;
for (int i = 0; i < 5; i++)
{
	array = new int[10];
}
delete[] array;

Здесь налицо утечка при выделении памяти для первых 4 массивов. Утекает 160 байт. Последний массив удаляется корректно. Итак, утечка строго в одной строке:

array = new int[10];

Пример 2.

class Test
{
public:
	Test()
	{
		a = new int[100];
		b = new int[300];
	}
	~Test()
	{
		delete[] a;
		delete[] b;
	}

private:
	int* a;
	int* b;
};

int main()
{
	Test* test = new Test;

	return 0;
}

Здесь утечек уже больше: не удаляется память для a (400 байт), для b (1200 байт) и для test (16 байт для x64). Впрочем, удаление a и b в коде предусмотрено, но его не происходит из-за отсутствия вызова деструктора Test. Таким образом, утечек три, но ошибка, приводящая к этим утечкам, всего одна, и она порождается строкой

Test* test = new Test;

При этом в коде класса Test ошибок нет.

Пример 3.

Пусть есть класс Qt, примерно такой:

class InfoRectangle : public QLabel
{
	Q_OBJECT

public:
	InfoRectangle(QWidget* parent = nullptr);

private slots:
	void setInfoTextDelayed();

private:
	QTimer* _textSetTimer;
};
InfoRectangle::InfoRectangle(QWidget* parent)
	: QLabel(parent)
{
	_textSetTimer = new QTimer(this);
	_textSetTimer->setInterval(50);
	connect(_textSetTimer, &QTimer::timeout, this, &InfoRectangle::setInfoTextDelayed);
}

void InfoRectangle::setInfoTextDelayed()
{
	// do anything
	setVisible(true);
}

Пусть также где-то в коде затесалось выделение памяти:

InfoRectangle* rectangle = new InfoRectangle();

Будет ли являться это утечкой, если явно не вызван delete? Это зависит от того, включен ли объект в иерархию объектов Qt. Если объект включён одним из следующих примерных вызовов, то нет, не утечка:

mnuLayout->addWidget(rectangle);
rectangle->setParent(this);

В остальных же случаях – утечка. Причем если мы будем считать точное количество утечек в этом примере, то можем наткнуться на неожиданный вывод: утечек больше, чем можно сначала предположить. Очевидная утечка – выделение памяти для InfoRectangle. Побочная утечка – выделение памяти для QTimer, несмотря на включение объекта _textSetTimer в иерархию объектов Qt. А вот утечка, которая совсем не очевидна – вызов функции connect.

Дело в том, что в ее реализации вызовом new всё же создается некий объект:
template <typename Func1, typename Func2>
    static inline QMetaObject::Connection connect(
const typename QtPrivate::FunctionPointer<Func1>::Object *sender, Func1 signal,
                const typename QtPrivate::FunctionPointer<Func2>::Object *receiver, Func2 slot,
                Qt::ConnectionType type = Qt::AutoConnection)
    {
        typedef QtPrivate::FunctionPointer<Func1> SignalType;
        typedef QtPrivate::FunctionPointer<Func2> SlotType;

        const int *types = nullptr;
        if (type == Qt::QueuedConnection || type == Qt::BlockingQueuedConnection)
            types = QtPrivate::ConnectionTypes<typename SignalType::Arguments>::types();

        return connectImpl(sender, reinterpret_cast<void **>(&signal),
                           receiver, reinterpret_cast<void **>(&slot),
                           new QtPrivate::QSlotObject<Func2, typename QtPrivate::List_Left<
typename SignalType::Arguments, SlotType::ArgumentCount>::Value,
                                          	typename SignalType::ReturnType>(slot),
                            type, types, &SignalType::Object::staticMetaObject);
    } 

Таким образом, утечки памяти могут обнаруживаться в весьма неожиданных местах. А могут и вовсе не обнаруживаться, в зависимости от используемого инструментария. К примеру, если инструментарий не способен проникнуть внутрь кода Qt, то он не сможет ни обнаружить утечку в connect, ни разобраться с иерархиями Qt, а последнее, в свою очередь, уже чревато ложными срабатываниями.

Зафиксируем еще один важный момент: причина утечки одна, а самих утечек при этом может быть много. Этот нюанс важен при обсуждении инструментариев  по отлову утечек. Идеальный инструментарий – тот, который, не давая ложных срабатываний и не пропуская утечек, указывает именно на первопричины утечек, то есть на каждую причину выдает ровно одну строку кода, ей соответствующую (а не целую кучу). То есть делает так, как делал бы человек. Забегая вперед, скажем, что таких инструментариев на данный момент не существует.

Раз их не существует, то как сравнивать между собой разные, не всегда коррелирующие результаты поиска утечек, полученные разными реальными инструментами? Мы ведь хотим сравнения…

Для этого мы взяли реальный пример – конкретную ревизию репозитория, для которой было точно известно, что утечки есть, и начали подробно с ней работать. Мы взяли один типичный сценарий работы пользователя с нашим приложением и начали на нём запускать все возможные динамические инструменты отлова утечек. Запускали многократно с разными настройками, детально анализируя полученные отчеты об ошибках. В итоге сформировали итоговый список ошибок в коде, приведших к утечкам памяти. Этот список назвали эталонным и посчитали, что других ошибок (ненайденных) нет. И в дальнейшем результаты, полученные каждой отдельной утилитой поиска утечек, сравнивали с эталонным списком ошибок.

Проект

Размер кода

Сценарий работы пользователя

Ревизия репозитория

Кол-во ошибок в эталоне

Суммарный объем утекающей памяти

Конкретный проект

1.5 млн строк

Конкретный сценарий: запускаем ПО, жмем на кнопку 1, потом на кнопку 2, ждем завершения вычислений, закрываем ПО

конкретная

7

253 кБ

Таблица 1. Эталон поиска утечек памяти.

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

Intel Inspector

Intel Inspector – графическая утилита, удобно встраиваемая в Visual Studio и позволяющая в виде кликабельного списка выдавать места в коде с предполагаемыми утечками оперативной памяти проверяемого приложения и некоторыми другими проблемами памяти. В сценарии отлова утечек памяти Intel Inspector использует динамический анализ, а это значит, что если в процессе работы приложения код с утечками памяти не будет вызван, то и проблем в нем не будет найдено.

Установка

Intel Inspector входит в состав пакета Intel Parallel Studio 2019, при этом есть возможность установить только сам Intel Inspector, убрав галочки с остальных компонентов дистрибутива при установке. Visual Studio 2019 должна быть закрыта в момент установки Intel Parallel Studio. После установки, Intel Inspector будет автоматически встроен в Visual Studio и должен появиться на панели инструментов (рис. 1).

Рис. 1. Начало работы с Intel Inspector`ом
Рис. 1. Начало работы с Intel Inspector`ом

Если значок Intel Inspector’а не виден на панели инструментов, нужно щёлкнуть правой кнопкой мыши где-нибудь на этой панели инструментов и поставить галочку «Intel Inspector».

Запуск

При нажатии на кнопку-значок появится вкладка Intel Inspector с выбором глубины анализа. Выбираем первый пункт «Detect Leaks» и включаем все галочки, соответствующие всем видам анализа (рис. 2). Если какие-то галочки пропустить, то, к сожалению, есть риск, что не все утечки будут найдены.

Рис. 2. Вкладка Intel Inspector`а для его настройки и запуска
Рис. 2. Вкладка Intel Inspector`а для его настройки и запуска

Далее нажимаем кнопку «Start», через некоторое время откроется приложение. В нем нужно запустить тот или иной сценарий работы, а лучше все сразу (то есть, как следует «погонять» приложение), затем закрыть. Чем больше на разных параметрах, в разных режимах и в разных сценариях проработает приложение, тем больше утечек памяти будет найдено. И это общий принцип для всех механизмов поиска утечек, использующих динамический анализ. Как мы уточнили ранее, в целях сравнения мы запускали только эталонный сценарий тестирования (см. табл. 1). Итак, после закрытия приложения Intel Inspector слегка задумывается и в итоге выдаёт отчёт следующего вида (рис. 3):

Рис. 3. Пример результатов анализа ПО на утечки памяти с помощью Intel Inspector.
Рис. 3. Пример результатов анализа ПО на утечки памяти с помощью Intel Inspector.

В отчете выдаются кликабельный и сортируемый список утечек, размеры утечек, места в коде с утечками, call-stack и многое другое.  Короче, форма выдачи результатов весьма и весьма на уровне. Все очень быстро понимается и усваивается. Все это – внутри IDE!

Это будет работать, если есть отладочная информация. То есть debug работать будет, а release нет. В С++-приложениях часто бывает так, что работа в режиме debug намного медленнее, чем в release (мы фиксировали разницу в скорости  до 20 раз), и пользоваться debug'ом очень некомфортно. Однако на этот случай есть лайфхак – собрать версию release (быструю, со всеми ключами оптимизации), дополнительно включив в нее отладочную  информацию. Это позволяет Intel Inspector'у подсветить строки в исходном коде, где он предполагает наличие утечек. О том, как включить в release отладочную информацию, написано здесь.

Результаты

Мы провели сравнение скоростных характеристик работы приложения в разных режимах работы: с Intel Inspector (будем называть его Инспектор) и без него, в debug и release. Тестирование проводилось на эталонном примере (см. табл 1).

Конфигурация

Среднее время теста, с

Замедление
работы, что привносит Инспектор, раз

Без Инспектора

С Инспектором

Release c отладочной информацией

10

70

7

Debug

101

973

9,6

Таблица 2. Время тестирования с учётом работы Intel Inspector`а

По таблице можно видеть, что проверка на утечки памяти требует весьма существенного времени. Особенно этот факт начинает играть существенными красками в свете непрерывной интеграции. То есть, если, к примеру, тот сценарий, на котором мы проверяли, включен в состав тестов регрессионного тестирования, то он в них выполняется за 10 секунд. Если же мы захотим на этом же сценарии прогонять поиск утечек памяти (в debug), то это займет примерно в 100 раз больше времени. Прогонять каждую ночь (а тем более, по каждому коммиту) тесты становится невозможно.

Самое главное – что можно сказать по качеству найденных утечек? Действительно ли они являются утечками? Нет ли утечек, не замеченных Intel Inspector`ом?

Конфигурация

Кол-во ошибок в эталоне: n

Найдено утечек

Пропущено ошибок из эталона: r

Точность: (n-r)/n

Избыточность: N/n

Всего: N

Верных

Ложных

Release c отладочной информацией

7

192

168

24

0

1 (100%)

27 раз

Debug

7

129

107

22

0

1 (100%)

18 раз

Таблица 3. Результаты работы Intel Inspector

Да, Intel Inspector действительно способен найти реальные утечки памяти. Это долго и мучительно, но он их находит. Пропусков утечек памяти мы не зафиксировали. При этом в итоговом отчете, который формирует Intel Inspector, бывает так, что на каждую строчку кода, где реально совершена ошибка, выводится куча строчек, которые «породились» этой ошибкой (как в примерах 2 и 3, см. выше).

Если ликвидировать все такие реальные ошибки, то Intel Inspector все равно будет показывать еще немало утечек, и все они – ложные. Более того, по таблице видно, что этих ложных срабатываний в release больше, чем в debug. И в этом случае, судя по всему, сработала оптимизация при компиляции – она скрыла от Инспектора некоторые детали, и Инспектор запутался.

Приведем несколько произвольных примеров ложных срабатываний.

Пример 1. Утечки в системных dll.

Intel Inspector может обнаружить вот такие вот странные утечки в подгружаемых системных dll, с таким интересным стеком. К проверяемому нами коду такие утечки вообще отношения не имеют, даже если по факту там утечки и есть.

Рис. 4. Утечки в системных dll.
Рис. 4. Утечки в системных dll.

Пример 2. aligned_malloc.

m_pVitData = (VITDEC_DATA*)_aligned_malloc(sizeof(VITDEC_DATA), 16);
m_pDcsnBuf = (byte*)_aligned_malloc(64 * (VITM6_BUF_LEN + VITM6_MAX_WND_LEN), 16);
...
_aligned_free(m_pDcsnBuf);
_aligned_free(m_pVitData);

К счастью, подобная "утечка" находится только в release, а в debug нет.

Пример 3. Pragma.

#pragma omp parallel for schedule(dynamic)
for (int portion = 0; portion < portionsToProcess; ++portion)
{
	…
}

Утечка показывается именно в строке с директивой #pragma!

Возможно, какими-то настройками (внутри Intel Inspector, внутри VS, переменные окружения и т.д.) можно победить этот вал ложных утечек, но из коробки – точно нет. Возможно также, что на маленьких и простых приложениях (<50000 строк кода) таких проблем с Intel Inspector не будет. На серьезных же приложениях – точно будут, к гадалке не ходи.

Вывод

Intel Inspector – штука довольно удобная и полезная, способная найти все утечки (если прогнать все сценарии), выдающая относительно немного ложных срабатываний. Работа в конфигурации release с отладочной информацией довольно быстра, но выдает больше ложных срабатываний (а значит, больше ручной работы по их проверке), чем debug. При этом работа в конфигурации debug фантастически медленна.

Что касается стабильности работы, то Intel Inspector здесь не может показать выдающиеся результаты. Иногда в процессе тестирования бывают падения, иногда зависания, причем на ровном месте. Иногда нам попадались такие важные для нас проекты и сценарии работы пользователя, когда  вообще не получалось даже «завести» Intel Inspector, не то, что «доехать» на нём до получения результатов.

Visual Leak Detector

Visual Leak Detector (далее VLD) – маленькая библиотека, включаемая в исходный код каждого проекта и выводящая в окно Output (IDE Visual Studio 2019) отчёт по утечкам памяти.

Установка

  1. Убедиться, что Visual Studio не запущена.

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

  3. Далее установить последний VLD (на момент написания статьи vld-2.5.1-setup.exe) по умолчанию, оставив при установке все галочки включёнными (на добавление в Path и встраивание в Visual Studio). Установщик можно скачать отсюда.

  4. На момент написания статьи в дистрибутиве VLD нет нужных dll-файлов для работы с Visual Studio 2019, потому необходимо скопировать dbghelp.dll из папки C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\IDE\Extensions\TestPlatform\Extensions\Cpp\x64 в папку C:\Program Files (x86)\Visual Leak Detector\bin\Win64.

  5. Нужно создать заголовочный файл примерно со следующим содержанием:

    #pragma once
    
    //#define LEAKS_DETECTION
    
    #ifdef LEAKS_DETECTION
    #include <vld.h>
    #endif

    Как видно, пока что это пустой файл, проверка на утечки памяти в нем выключена.

  6. В любой файл реализации (сpp) нужно включить этот новый заголовочник. Это нужно сделать только для одного файла внутри проекта, и для каждого проекта в solution.

Запуск

Достаточно раскомментировать в заголовочном файле строчку

#define LEAKS_DETECTION

и собрать solution. После этого можно запускать (F5) приложение и прогонять разные сценарии, где могут быть утечки памяти. Запускать можно только в конфигурации debug. Release c отладочной информацией работать не будет.

После закрытия проверяемого приложения VLD выведет отчет в окно Output. Отчет содержит список утечек, кликабельный call-stack по каждой утечке, размеры утечек.

Пример того, что выводит VLD
---------- Block 652047 at 0x0000000027760070: 8787200 bytes ----------
  Leak Hash: 0x02B5C300, Count: 1, Total 8787200 bytes
  Call Stack (TID 30996):
    ucrtbased.dll!malloc()
    d:\agent\_work\63\s\src\vctools\crt\vcstartup\src\heap\new_array.cpp (29): SniperCore.dll!operator new[]()
    D:\SOURCE\SAP_Git\sap_win64\core\alg\fbg\ddec\S2Ldfg.cpp (445): SniperCore.dll!CS2Ldfg::CreateLLRTbls() + 0xD bytes
    D:\SOURCE\SAP_Git\sap_win64\core\alg\fbg\ddec\S2Ldfg.cpp (217): SniperCore.dll!CS2Ldfg::SetModeEB()
    D:\SOURCE\SAP_Git\sap_win64\core\alg\fbg\ddec\S2Ldfg.cpp (1447): SniperCore.dll!CS2Ldfg::Set() + 0xA bytes
    D:\SOURCE\SAP_Git\sap_win64\core\alg\fbg\ddec\ddec.cpp (509): SniperCore.dll!DFBase::instanceS2Dec()
    D:\SOURCE\SAP_Git\sap_win64\core\alg\fbg\ddec\ddec.cpp (58): SniperCore.dll!DFBase::DFBase() + 0xF bytes
    D:\SOURCE\SAP_Git\sap_win64\core\alg\fbg\ddec\ddec.cpp (514): SniperCore.dll!DgbS5FecAnlzr::DgbS5FecAnlzr() + 0xA bytes
    D:\SOURCE\SAP_Git\sap_win64\core\alg\fbg\fbganalyser.cpp (45): SniperCore.dll!TechnicalLayer::FBGAnalyser::FBGAnalyser() + 0x21 bytes
    D:\SOURCE\SAP_Git\sap_win64\core\engine\handlers\fbganalysishandler.cpp (218): SniperCore.dll!TechnicalLayer::FBGAnalysisHandler::init() + 0x2A bytes
    D:\SOURCE\SAP_Git\sap_win64\core\engine\handlers\fbganalysishandler.cpp (81): SniperCore.dll!TechnicalLayer::FBGAnalysisHandler::enqueueRequest()
    D:\SOURCE\SAP_Git\sap_win64\core\engine\threadedhandler2.cpp (57): SniperCore.dll!TotalCore::ThreadedHandler2::run()
    Qt5Cored.dll!QTextStream::realNumberPrecision() + 0x89E8E bytes
    kernel32.dll!BaseThreadInitThunk() + 0xD bytes
    ntdll.dll!RtlUserThreadStart() + 0x1D bytes
  Data:
    00 00 00 00    01 01 01 01    01 01 01 02    02 02 02 02     ........ ........
    02 02 03 03    03 03 03 03    03 04 04 04    04 04 04 04     ........ ........
    05 05 05 05    05 05 05 05    06 06 06 06    06 06 06 07     ........ ........
    07 07 07 07    07 07 08 08    08 08 08 08    08 09 09 09     ........ ........
    09 09 09 09    0A 0A 0A 0A    0A 0A 0A 0B    0B 0B 0B 0B     ........ ........
    0B 0B 0C 0C    0C 0C 0C 0C    0C 0D 0D 0D    0D 0D 0D 0D     ........ ........
    0E 0E 0E 0E    0E 0E 0E 0E    0F 0F 0F 0F    0F 0F 0F 10     ........ ........
    10 10 10 10    10 10 11 11    11 11 11 11    11 12 12 12     ........ ........
    EE EE EE EE    EF EF EF EF    EF EF EF F0    F0 F0 F0 F0     ........ ........
    F0 F0 F1 F1    F1 F1 F1 F1    F1 F2 F2 F2    F2 F2 F2 F2     ........ ........
    F3 F3 F3 F3    F3 F3 F3 F3    F4 F4 F4 F4    F4 F4 F4 F5     ........ ........
    F5 F5 F5 F5    F5 F5 F6 F6    F6 F6 F6 F6    F6 F7 F7 F7     ........ ........
    F7 F7 F7 F7    F8 F8 F8 F8    F8 F8 F8 F9    F9 F9 F9 F9     ........ ........
    F9 F9 FA FA    FA FA FA FA    FA FB FB FB    FB FB FB FB     ........ ........
    FC FC FC FC    FC FC FC FC    FD FD FD FD    FD FD FD FE     ........ ........
    FE FE FE FE    FE FE FF FF    FF FF FF FF    FF 00 00 00     ........ ........


---------- Block 2430410 at 0x000000002E535B70: 48 bytes ----------
  Leak Hash: 0x7062B343, Count: 1, Total 48 bytes
  Call Stack (TID 26748):
    ucrtbased.dll!malloc()
    d:\agent\_work\63\s\src\vctools\crt\vcstartup\src\heap\new_scalar.cpp (35): SniperCore.dll!operator new() + 0xA bytes
    C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Tools\MSVC\14.28.29333\include\xmemory (78): SniperCore.dll!std::_Default_allocate_traits::_Allocate()
    C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Tools\MSVC\14.28.29333\include\xmemory (206): SniperCore.dll!std::_Allocate<16,std::_Default_allocate_traits,0>() + 0xA bytes
    C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Tools\MSVC\14.28.29333\include\xmemory (815): SniperCore.dll!std::allocator<TotalCore::TaskResult *>::allocate()
    C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Tools\MSVC\14.28.29333\include\vector (744): SniperCore.dll!std::vector<TotalCore::TaskResult *,std::allocator<TotalCore::TaskResult *> >::_Emplace_reallocate<TotalCore::TaskResult * const &>() + 0xF bytes
    C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Tools\MSVC\14.28.29333\include\vector (708): SniperCore.dll!std::vector<TotalCore::TaskResult *,std::allocator<TotalCore::TaskResult *> >::emplace_back<TotalCore::TaskResult * const &>() + 0x1F bytes
    C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Tools\MSVC\14.28.29333\include\vector (718): SniperCore.dll!std::vector<TotalCore::TaskResult *,std::allocator<TotalCore::TaskResult *> >::push_back()
    D:\SOURCE\SAP_Git\sap_win64\include\core\engine\task.h (119): SniperCore.dll!TotalCore::LongPeriodTask::setTmpResult()
    D:\SOURCE\SAP_Git\sap_win64\include\core\engine\discretestephandler.h (95): SniperCore.dll!TotalCore::DiscreteStepHandler::setResult()
    D:\SOURCE\SAP_Git\sap_win64\core\engine\handlers\prmbdtcthandler.cpp (760): SniperCore.dll!TechnicalLayer::PrmbDtctHandler::setContResult() + 0x1A bytes
    D:\SOURCE\SAP_Git\sap_win64\core\engine\handlers\prmbdtcthandler.cpp (698): SniperCore.dll!TechnicalLayer::PrmbDtctHandler::processPortion()
    D:\SOURCE\SAP_Git\sap_win64\core\engine\threadedhandler2.cpp (109): SniperCore.dll!TotalCore::ThreadedHandler2::tryProcess()
    D:\SOURCE\SAP_Git\sap_win64\core\engine\threadedhandler2.cpp (66): SniperCore.dll!TotalCore::ThreadedHandler2::run()
    Qt5Cored.dll!QTextStream::realNumberPrecision() + 0x89E8E bytes
    kernel32.dll!BaseThreadInitThunk() + 0xD bytes
    ntdll.dll!RtlUserThreadStart() + 0x1D bytes
  Data:
    10 03 51 05    00 00 00 00    B0 B4 85 09    00 00 00 00     ..Q..... ........
    60 9D B9 08    00 00 00 00    D0 1B 24 06    00 00 00 00     `....... ..$.....
    30 B5 4F 11    00 00 00 00    CD CD CD CD    CD CD CD CD     0.O..... ........

В конце отчёта присутствует краткий итог в виде:

Visual Leak Detector detected 383 memory leaks (253257876 bytes).
Largest number used: 555564062 bytes.
Total allocations: 2432386151 bytes.
Visual Leak Detector is now exiting.

Или, если утечек нет,

No memory leaks detected.
Visual Leak Detector is now exiting.

Результаты

Мы провели сравнение скоростных характеристик работы приложения  в конфигурации debug в разных режимах работы: с VLD и без него. Как было сказано, в конфигурации release (пусть даже и с отладочной информацией) vld работать не может. В табл. 4 замеры времени выполнения для release приводятся исключительно для сравнения с debug. Тестирование проводилось на эталонном примере (см. табл. 1).

Конфигурация

Среднее время теста, с

Замедление работы, что привносит VLD, раз

Без VLD

С VLD

Debug

101

172

1,7

Release c отладочной информацией

10

-

-

Таблица 4. Время тестирования с учётом работы VLD

Что можно сказать по качеству найденных утечек? Действительно ли они являются утечками? Нет ли утечек, не замеченных VLD?

Конфигурация

Кол-во ошибок в эталоне: n

Найдено утечек

Пропущено ошибок из эталона: r

Точность: (n-r)/n

Избыточность: N/n

Всего: N

Верных

Ложных

Debug

7

185

185

0

0

1 (100%)

26 раз

Таблица 5. Результаты работы VLD

Да, VLD находит реальные утечки памяти. Пропусков утечек памяти мы не зафиксировали. При этом в итоговом отчете, который формирует VLD, бывает так, что на каждую строчку кода, где реально совершена ошибка, выводится очень большая куча строчек, которые «породились» этой ошибкой (как в примерах 2 и 3, см. выше). Из-за того, что эти утечки нельзя никак сортировать (или группировать), оказывается не очень приятно работать с такой большой плоской «простыней». Да и вообще, как поначалу можно доверять тому списку утечек, где фигурирует такая вот утечка:

connect(arrowKeyHandler, &ArrowKeyHandler::upPressed,
			[this] { selectNeighbourSignal(TopSide); }); 

Потом, разбираясь, оказывается, что деструктор класса всё же не вызывается, и утечка в функции connect действительно есть (см. пример 3). Но поначалу этому списку сложно поверить из-за большой избыточности.

Если ликвидировать все такие реальные ошибки, то VLD честно скажет, что утечек нет. И этот факт крайне важен для continuous integration.

Вывод

Visual Leak Detector – штука очень простая и очень полезная, способная найти все утечки (если прогнать все сценарии) и при этом не выдающая ложных срабатываний. Прогон сценариев в VLD довольно медленный, однако, он всё же быстрее, чем в Intel Inspector в конфигурации debug. Плоский, не очень дружественный и «простынообразный» вывод результатов способен запутать своей объемностью и дубликатами, однако со временем и к нему можно привыкнуть и даже использовать в continuous integration.

По стабильности работы больших нареканий нет. Однако всё же было несколько важных для нас проектов и сценариев работы в них, когда vld выдавал исключение и категорически отказывался работать. А жаль, если бы не это обстоятельство, то ценность этой утилиты была бы намного больше.

Снимки оперативной памяти в VS 2019

В IDE Visual Studio 2019 есть собственный встроенный компонент для диагностики проблем – Diagnostic Tools. В его составе есть механизм получения снимков памяти (snapshots). С его помощью можно находить утечки памяти как разницу (дельта) между снимками.  Само собой, чтобы дельта показывала именно утечки, надо делать снимки в определенные, далеко не случайные моменты.

Запуск

Запустите приложение в отладчике (в конфигурации debug или release c отладочной информацией). При запуске  должна по умолчанию появиться панель Diagnostic Tools. Выберите на этой панели вкладку Memory Usage, нажмите кнопку Heap Profiling и дальше делайте снимки кнопкой Take Snapshot.

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

Перед созданием каждого снимка удобно либо временно приостанавливать работу приложения кнопкой Break All при отладке, либо ставить точку останова на нужное место в коде, и она сработает в нужный момент, после чего можно делать снимок.

Рис. 5. Работа со снимками памяти.
Рис. 5. Работа со снимками памяти.

Для просмотра результатов нажмём на прирост памяти между снимками (см. Рис. 5, где стрелочка). В появившейся вкладке в области редактора кода выберем ViewMode -> Stacks View (вместо Types View), и через некоторое время формирования отчёта увидим интерактивное дерево вызовов:

Рис. 6. Работа со снимками памяти, call-stack.
Рис. 6. Работа со снимками памяти, call-stack.

Результаты

Надо честно признать, что работать со снимками памяти при нахождении утечек очень сложно в больших приложениях на Qt: очень большой лог, глубоко завязанный на специфику Qt. Из него можно вычленять крупицы реальных ошибок, но это довольно трудоемко. Взяв наш эталонный сценарий тестирования (см. табл. 1), мы не смогли за пару часов найти ни одной утечки. Зато смогли добиться того, чтобы инструмент самопроизвольно отключался с ошибкой.

Вывод

Для поиска утечек памяти механизмом снимков можно пользоваться только, если никакой другой инструментарий попросту не запускается на вашем тесте (падает, зависает и т.д.). Даже если механизмом снимков удастся найти утечку, скорее всего, это потребует колоссального ручного анализа. Однако для поиска простых утечек (вроде выделения большого куска памяти в маленьком приложении) снимки памяти могут пригодиться.

PVS-Studio

Последняя из рассматриваемых в данной статье утилита, которая может помочь в нахождении утечек памяти, - PVS-Studio. Она делает статический анализ кода, не требующий запуска приложения. Анализ может запускаться и для одного выделенного проекта, и для всех проектов в solution. Также он может запускаться при каждом новом сохранении редактируемого файла исходного кода, «инкрементно», сразу указывая на сомнительные места в новом коде.

Установка

Проблем с установкой нет. В результате установки PVS-Studio оказывается встроенной в Visual Studio 2019, в меню «Extensions».

Запуск

Для запуска всего solution`а вызываем команду Extensions->PVS-Studio->Check. Результат проверки выдается во вкладке «PVS-Studio» и содержит список потенциальных ошибок в коде, распределённых по вкладкам с «критичностью» High, Medium и Low.

Этот объемный результирующий список будет содержать не только утечки памяти, а и все остальные ошибки в коде, что PVS-Studio посчитает ошибками. Чтобы оставить только то, что касается только утечек памяти, нужно фильтровать список по следующим кодам: V599, V680, V689, V701, V772, V773, V1005, V1023 (более подробно см. здесь).

Для фильтрации нужно зайти в настройки Visual Studio в меню Tools -> Options -> PVS-Studio и на вкладке «Detectable Errors (C++)» выставить нужные галочки, убрав остальные (при этом удобно сначала использовать команду «Hide All», а потом уже ставить галочки) – Рис. 8. Также нужно убрать галочки из других групп и вкладки «Detectable Errors (C#)» (выбирая «Hide All» или «Disabled»).

Рис. 8. Фильтрация списка найденных утилитой PVS-Studio ошибок.
Рис. 8. Фильтрация списка найденных утилитой PVS-Studio ошибок.

Чтобы показать все сообщения с выбранными кодами ошибок, нужно убедиться, что в окне PVS-Studio над сообщениями все кнопки High, Medium и Low включены.

Результаты

Итак, для поиска утечек памяти был запущен анализ на проекте, включающем около 1.5 млн строк кода и 2269 файлов кода. Анализ производился на Intel Core i7 4790K. Конфигурация кода (debug или release) значения не имеет, поскольку анализ статический (если более точно, разница есть из-за условной компиляции, но она непринципиальна).

Время анализа

Кол-во ошибок в эталоне: n

Найдено утечек

Пропущено ошибок из эталона: r

Точность: (n-r)/n

Всего

Верных

Ложных

30 мин

7

2

0

2

7

0 %

Таблица 6. Поиск утечек памяти утилитой PVS-Studio

Вывод

Для поиска утечек памяти этой утилитой можно пользоваться только, если под рукой нет чего-то более заточенного под утечки памяти (Intel Inspector, VLD). Она не способна находить все утечки, но выдает ложные срабатывания. Это не удивительно, поскольку утилита PVS-Studio никогда и не заявлялась как специализированный инструмент поиска утечек.

Сравнение работы инструментов для поиска утечек памяти

Подводя итоги, можно однозначно выделить в качестве лучших 2 инструмента для поиска утечек – Intel Inspector и Visual Leak Detector. На основании проведенного тестирования мы получаем следующую их сравнительную таблицу:  

Intel Inspector

VLD

Вид анализа

Динамический

Динамический

Стабильность работы

Средняя

Высокая

На любом ли примере (сценарии) может отработать

Нет

Нет

Удобство использования

Среднее

Низкое

Замедление debug

9.6 раз

1,7 раз

Замедление release с отладочной информацией

7 раз

-

Находит ли реальные утечки в debug

Да, все. Избыточность результатов – 18 раз.

Да, все. Избыточность результатов – 26 раз.

Находит ли реальные утечки в release с отладочной информацией

Да, все. Избыточность результатов – 27 раз.

-

Ложные срабатывания в debug

Да, немного

Нет

Ложные срабатывания в release с отладочной информацией

Да, немного

-

Можно ли использовать в Continuous Integration

Нет

Да

Таблица7. Сравнение Intel Inspector и VLD.

Место №1 в рейтинге целесообразно отдать VLD, поскольку он не выдает ложных срабатываний, более стабилен в работе и более подходит для использования в сценариях непрерывной интеграции.

Источник: https://habr.com/ru/post/542656/


Интересные статьи

Интересные статьи

Привет, друзья! Меня зовут Петр, я представитель малого белорусского бизнеса со штатом чуть более 20 сотрудников. В данной статье хочу поделиться негативным опытом покупки 1С-Битрикс. ...
Компании переполнили рынок товаров и услуг предложениями. Разнообразие наблюдается не только в офлайне, но и в интернете. Достаточно вбить в поисковик любой запрос, чтобы получить подтверждение насыще...
В статье есть тяжелые фото, так что убрал под спойлеры. Введение Проблема запоминания цифровой информации возникла раньше, чем появились собственно компьютеры. Перед тем, как говорить, о конк...
Напомню, что мы уже поговорили о блокировках отношений, о блокировках на уровне строк, о блокировках других объектов (включая предикатные), и о взаимосвязи разных типов блокировок. Сегодня я з...
Один из самых острых вопросов при разработке на Битрикс - это миграции базы данных. Какие же способы облегчить эту задачу есть на данный момент?