Долгие годы С++ программисты, пишущие под Linux язвительно пеняли разработчикам на С++ под Windows отсутствием в Visual Studio нормального профилировщика памяти. Вот в Линуксе, дескать, есть Valgrind, который решает все проблемы, а в студии что: расставляй какие-то макросы, анализируй какие-то логи — мрак. Клевета! Хотя и правда. Вернее, это было правдой до выхода Visual Studio 2015, в которой наконец-то (ура 3 раза!) присутствует нормальный профилировщик памяти, позволяющий ловить утечки памяти с закрытыми глазами, одной левой и даже не просыпаясь!

В этой статье мы посмотрим, что он умеет и как им пользоваться.



Запускаем Visual Studio 2015, создаём новый консольный проект на С++ и пишем в него следующий код:

#include<iostream>
int main()
{
	for (;;)
	{
		std::cout << "Hello, Habr!";
		getchar();
	}
    return 0;
}

Теперь запускаем приложение под отладчиком (F5) и видим появившуюся в Visual Studio панель Diagnostic Tool (см. скриншот выше).

Она показывает загрузку процессора и памяти, но нам интересно не это. Самое ценное в этой панели — нижняя часть, позволяющая нам создавать снимки памяти приложения в любой момент времени. По-умолчанию эта функциональной отключена (поскольку затормаживает работу приложения), чтобы её включить нужно нажать кнопку «Enable snapshots» и перезапустить приложение под отладчиком.

Теперь нам становится доступной кнопка «Take Snapshot», давайте её нажмём.



И у нас появился первый снимок памяти! Мы можем кликнуть по нему дважды и посмотреть, что там внутри:



Мы видим список всех выделений памяти, которые произошли в нашем процессе, типы созданных переменных, их количество и размер в байтах. Приложение наше простое, как двери, но всё же… Что это за массив char[] размером в 100 байт? Как узнать, где он создаётся? Просто кликаем по нему дважды — попадаем в список экземпляров объектов этого типа. У нас он всего один. Внизу окна мы видим стек вызовов, по ходу выполнения которого был аллоцирован данный блок памяти. Смотрим кто на вершине этого стека:



Итак, это функция main(), строка №9. Двойной клик переведёт нас прямо к коду.



О боже, как же так! Оказывается, я только собирался написать тот простой код, который привёл сверху, а по ходу дела создал в цикле массив на 100 байт, который нигде не удаляется и приводит к утечке памяти. Даже и не знаю как бы я её нашел, если бы не новый профилировщик Visual Studio!

«Ладно, хватит прикалываться» — скажет практично настроенный читатель — «Нашел он выделение одного массива в программе из 7 строк, где никакой другой памяти не выделяется. У меня вот в проекте 150 тыщ классов и кода как текста в „Войне и мире“, ты попробуй тут найди где там что утекает!».

А давайте попробуем. Для реализма создадим новый MFC-проект, который тянет за собой (сюрприз!) — MFC. Проект создаём стандартным визардом, ничего не меняя. И вот у нас пустой проект из 55 файлов — да здравствует «минималистичность» MFC. Хорошо хоть билдится.

Найдём метод CMFCApplication1App::OnAppAbout() и допишем в него уже знакомую нам утечку памяти:

CMFCApplication1App::OnAppAbout()
{
	CAboutDlg aboutDlg;
	aboutDlg.DoModal();
	char* l = new char[100];
}

Теперь запустим это приложение под профилировщиком памяти. Как вы догадываетесь, уже по ходу запуска MFC навыделяет себе памяти. Сразу после запуска создадим первый снимок памяти, а дальше нажмём 10 раз кнопку «About». Каждый раз будет показан модальный диалог (что приведёт к некоторому количеству операций выделения и освобождения памяти) и, как вы догадались, каждый раз будет происходить утечка 100 байт памяти. В конце создадим ещё один снимок памяти — теперь у нас их два.



Первое, что мы видим, это разницу в количестве выделенной памяти — во втором снимке на 58 выделений больше, что в сумме составляет 15.71 КB. В основном это память выделенная MFC для своих внутренних нужд (прямо как в вашем проекте со 150 тысячами классов, да?), которая потом, наверное, будет MFC освобождена. Но нас интересует не она, а утечки памяти в нашем коде. Давайте откроем второй снимок памяти:



В принципе, уже отсюда можно делать кое-какие выводы: у нас есть 10 указателей на char, по 100 байт каждый — вполне вероятно что 10 выделений памяти связаны с 10-ю кликами по кнопке, а значит можно искать в коде число «100» ну или перейти по стеку вызовов в место выделения памяти для этого массива. Но ладно, усложним себе задачу — представим, что у нас здесь не 7 строк с указателями на выделенную память, а 700 или 7000. Среди них могут быть и блоки большего размера, и другие блоки, существующие в количестве 10 экземпляров. Как же нам отследить только то, что было создано между двумя снимками памяти? Элементарно — для этого есть комбик «Compare to» в верхней части окна. Просто выбираем там снимок №1 и видим только разницу между моментом перед первым кликом по кнопке и моментом после 10-го клика. Теперь табличка выглядит значительно чище, тут уже и слепой заметит, на что следует обратить внимание.



Плюс у нас есть сортировка по столбцам и поиск по типам данных.

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

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


  1. Door
    27.08.2015 14:20
    +3

    Я смотрел какое-то видео, увидел это окошко и именно из-за него захотел Visual Studio 2015! Ещё не пользовался, но — УРА, круто, наконец-то. Дождались!


  1. maaGames
    27.08.2015 15:29

    Вот и достойная причина перейти на 2015 студию. Спасибо!


  1. khim
    27.08.2015 15:55
    +3

    Это ужа даже больше, чем чистый Valgrind. Это скорее на LSAN похоже. И, как обычно, Microsoft отстаёт в части функциональности, но далеко впереди в части UI. Ничего похожего на ASAN/MSAN/TSAN пока ещё нету, как я понимаю? Не говоря уже о DFSANе… Но пользоваться удобно, не спорю.


  1. sergestus
    27.08.2015 16:26

    Очень полезная вещь! А почему в случае визуального сравнения двух снимков мы видим 58 выделений и 15.71 КВ, а в случае исползования «Compare to» — 10 выделений и 1 КВ?


    1. tangro
      27.08.2015 17:04

      Оно показывает в списке те типы, которые смогло распознать, если память выделялась не через типизированный new, а malloc-ом, то это будет показано под пунктом «Undeterminated type», он по-умолчанию отключен, но его можно включить — справа сверху кнопка фильтра.


  1. outcoldman
    27.08.2015 17:59

    Что-то я так и не понял, откуда взялась та строка, если вы ее не писали? :D


    1. tangro
      27.08.2015 19:40
      +6

      Ну так а кто же в коде специально утечки памяти пишет? Они сами появляются! :D


  1. maaGames
    27.08.2015 18:58
    +2

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


  1. beduin01
    27.08.2015 23:10

    Вопрос: D профайлить можно? Про плагин интеграции знаю, но интересует именно профайлер кода.


    1. tangro
      28.08.2015 11:02

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


      1. beduin01
        28.08.2015 13:32

        А с D эта штука дружит? Или только С++?


        1. tangro
          28.08.2015 19:01

          Без понятия


  1. aTwice
    28.08.2015 10:56

    А профилировщик умеет работать с переопределенным оператором new?