Боремся с утечками памяти (C++ CRT)

    Утечка памяти — довольно серьезная и опасная проблема. Быть может, пользователь и не заметит однократной утечки каких-нибудь 32Кб памяти (а ведь это целые 5% от 640Кб, которых «хватит всем»), но постоянно теряя сложные иерархические структуры или массивы размером больше INT_MAX (которые мы так любим создавать на 64-битной архитектуре) мы обречем его на страдания, а наш продукт на провал.

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

    А можно было бы просто «отдаться» автоматическому сборщику мусора, ценой потери производительности (и это не обязательно Managed C++, для Native C++ / C есть библиотеки сборки мусора, вот, например).

    Но мы поговорим о ситуации когда уже «всё плохо».

    Тогда задача сводится к обнаружению и исправлению возможных утечек — что касается исправления, тут обычно всё просто (delete или delete[]). А вот как обнаружить утечку? Гугл с радостью подскажет ответы, которые обычно сводятся к тому, что:
    • нужно использовать сторонние анализаторы утечек
    • придется изобретать велосипеды и самокаты
    • неплохо бы написать собственный менеджер памяти

    Но можно и проще, средствами Debug CRT.

    Шаг 1. Включение учета утечек


    Для этого нужно подключить хедер Debug CRT и включить использование Debug Heap Alloc Map:
    #ifdef _DEBUG
    #include <crtdbg.h>
    #define _CRTDBG_MAP_ALLOC
    #endif


    * This source code was highlighted with Source Code Highlighter.

    Теперь при выделении памяти через new или malloc() данные оборачиваются в следующую структуру (но на самом деле я чуть-чуть лукавлю, поле отвечающее за data не соответствует синтаксису struct и сама «структура» определена где-то внутри CRT и её описание программисту не доступно):

    typedef struct _CrtMemBlockHeader
    {
      struct _CrtMemBlockHeader * pBlockHeaderNext;
      struct _CrtMemBlockHeader * pBlockHeaderPrev;
      char* szFileName;
      int  nLine;
      size_t nDataSize;
      int nBlackUse;
      long lRequest;
      unsigned char gap[nNoMansLandSize];  
      unsigned char data[nDataSize];
      unsigned char anotherGap[nNoMansLandSize];
    } _CrtMemBlockHeader;


    * This source code was highlighted with Source Code Highlighter.

    Она содержит информацию об имени файла szFileName и строке nLine, где произошло выделение памяти, объем запрошенной памяти nDataSize, и, собственно сами данные data, обернутые в так называемую No Mans Land область. Сами BlockHeader'ы организованы в двусвязный список, что позволяет легко перечислить их, и, соответственно, выявить все операции выделения памяти, для которых не было соответствующей операции освобождения.

    Шаг 2. Перечисление утечек


    Нужна функция, которая пробежится по списку CrtMemBlockHeader'ов и выдаст нам информацию о проблемных местах:

    _CrtDumpMemoryLeaks();

    Тогда в окне Debug Output мы увидим следующую информацию:
    Detected memory leaks!
    Dumping objects ->
    {163} normal block at 0x00128788, 4 bytes long.
    Data: <  > 00 00 00 00
    {162} normal block at 0x00128748, 4 bytes long.
    Data: <  > 00 00 00 00
    Object dump complete.


    * This source code was highlighted with Source Code Highlighter.

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

    … а еще ту, которая просто не успела «вернуться» из глобальных объектов. И может быть глобальные объекты — это плохо, но сейчас свыкнемся с мыслью, что они есть, а значит из вывода _CrtDumpMemoryLeaks() нужно как-то их удалить. И решается это следующим приемом:

    int _tmain(int argc, _TCHAR* argv[])
    {
      _CrtMemState _ms;
      _CrtMemCheckpoint(&_ms);
     
      // some logic goes here...
      
       _CrtMemDumpAllObjectsSince(&_ms);
      return 0;
    }

    * This source code was highlighted with Source Code Highlighter.

    Мы записываем в специальную структуру начальное (текущее на момент входа в main) состояние памяти (_CrtMemCheckpoint), а перед завершением работы приложения выводим все оставшиеся объекты в памяти (_CrtMemDumpAllObjectsSince), созданные после момента _ms — они-то и есть «утечки». Теперь информация корректна, позаботимся об её удобстве.

    Шаг 3. Представление результатов


    Перенаправить вывод очень легко, здесь нам помогут функции _CrtSetReportMode и _CrtSetReportFile.

    _CrtSetReportMode( _CRT_WARN, _CRTDBG_MODE_FILE );
    _CrtSetReportFile( _CRT_WARN, _CRTDBG_FILE_STDOUT );


    * This source code was highlighted with Source Code Highlighter.

    Теперь вывод всех предупреждений (а таковым является любой вывод _CrtMemDumpAllObjectsSince) отправится прямиком на stdout. Вторым параметром функции _CrtSetReportFile можно поставить и реальный хендл файла.

    Почему не выводятся имена файлов и строки, где произошло выделение памяти? Так сложилось, что по версию Microsoft Visual C++ 6.0 за эту информацию отвечала следующее переопределение функции new в хедере crtdbg.h:

    #ifdef _CRTDBG_MAP_ALLOC
      inline void* __cdecl operator new(unsigned int s)
         { return ::operator new(s, _NORMAL_BLOCK, __FILE__, __LINE__); }
      #endif /* _CRTDBG_MAP_ALLOC */

    * This source code was highlighted with Source Code Highlighter.

    И, нетрудно догадаться, это не давало желаемого результата, __FILE__:__LINE__ всегда разворачивались в «crtdbg.h file line 512». А потом ребята из Microsoft вообще убрали эту «фичу», отдав её на откуп программисту. Ну и не страшно, ведь добиться этой функциональности можно одним определением:
    #define new new( _NORMAL_BLOCK, __FILE__, __LINE__)

    * This source code was highlighted with Source Code Highlighter.

    Котрое весьма желательно вынести в какой-нибудь общий заголовочный файл (подключаемый обязательно после crtdbg.h). Проблемы возникнут, если new уже был переопределен. Хотя, как видится мне, какое-либо разумное переопределение new не будет использовать CRT (иначе можно было бы использовать технику hook), и схема, в этом случае, вообще будет не применима, ну и ладно.

    В общем и целом теперь получили что хотели: вот пример вывода, но, думаю, и так очевидно, что там должно быть.

    Час расплаты


    Конечно, на организацию и поддержку CRT Internals структур требуется время и дополнительная память. Насколько же много?

    UPD: Все что ниже — справедливо только для Win32 (тестировалось на Vista SP1).

    Создаем 10 миллионов int с помощью new (40Mb памяти теоретически):
    Debug CRT ~500Mb 3 секуды
    Release ~160Mb 1 секунда

    Цифра в ~160Mb для релизной сборки может немнго удивить. Но это нормально — new выделяет память через функцию ОС HeapAlloc, которая выравнивает данные по кратным 16 адресам (для Win32). Выделяя память на один символ мы получаем еще 15 байт, с которыми даже можно сделать (но уж точно не нужно делать) что-нибудь нехорошее. Для дебага очень предсказуемый результат — добавим еще sizeof(_CrtMemBlockHeader) помноженный на 10 миллионов и получим, как раз, 500 мегабайт.

    Интересным эмпирическим результатом оказалось, что в релизе new int работает где-то на 10% медленнее, чем HeapAlloc на 4 байта, едва ли отличимо по скорости от new int() (инициализация значением по умолчанию, то есть нулем), и быстрее на 5-10% чем HeapAlloc с флагом HEAP_ZERO_MEMORY.

    Ну а теперь 128 тысяч int[256] через new int[256] (128Mb памяти теоретически):
    Debug CRT ~136Mb 172 мс
    Release ~128.5Mb 60 мс

    Результаты предсказуемые и вполне удовлетворительные. Отношение скорости 1:3 подтвердилось и на данных другого размера, в том числе при смешивании различных данных и частичном освобождении памяти. Но и без операций с динамической памятью Debug код работает в несколько раз медленнее Release!

    Вывод


    С утечками памяти можно разобраться практически голыми руками. Конечно, наш «сырой» вывод не будет так эффективен, как дерево утечек, или список мест кода сортированный по убыванию суммарной утечки (хотя это все можно сгенерить без труда по нашему выводу). Но для небольших проектов или задач может сделать свое дело. Да и в поддержке метод не нуждается (не совсем «написал и забыл», из-за переопределения new, но близко к тому), а уровень вхождения куда ниже, чем для серьезных анализаторов.

    Ну вот, пожалуй и всё. Разве что исходничек для воссоздания целостной картины.

    UPD: Метод не собирается конкурировать с внешними анализаторами, т.к. цели несколько разные, но упоминания о стоящих тулсах очень приветствуются (только, пожалуйста, без повторов).

    Комментарии 45

      +3
      непонятно какая платформа имеется ввиду и какой компилятор в разделе «Час расплаты». Может вы подразумеваете наличие одной платформы, одной ОС и одного компилятора? тогда поделитесь пожалуйста, а то я мож отстаю от времени.

      По поводу предложеного метода, думаю не очень пригоден для больших проектов, предпочитаю Valgrind для отладки и «умные указатели» в качестве реализации, естественно где это уместно и оправдано.
        0
        Спасибо за замечание; при тестировании использовалось MSVC9 на Vista SP2. Тот же порядок (но немного бОльшие цифры) выходят для тестового приложения запущенного на XP SP3. «Час расплаты» целиком относится только к Win32.

        Да, для больших (хотя это понятие для каждого своё) проектов этот метод не пригоден, согласен. А «умные указатели» тоже не стоит воспринимать как панацею, от «лишнего» add reference они не застрахованы (человеческий фактор), а найти его иногда крайне сложно (а, интересно, Valgrind справится? особенно меня интересует эта проблема для CComPtr).
          0
          CComPtr никогда не пользовался и не представляю как он устроен. Но полюбому поможет, так как память выделяет и освобождает сам Valgrind (я незнаю работает ли он с визуалстудией или ему нужен обязаельно POSIX, но мне кажется, что ему нужен POSIX и для Visual Stusio он получается непригоден, но не уверен),

          Вообще есть автоматиеские указатели, которы разрушают объект, когда последний из указателей выходит за область видимости. Много есть инструментов в Boost, Qt и новой спецификации С++. Могу сказать только про Qt, так как глубоко разбирался только с ними, при правильном подходе — можно спастить практически полностью от человеческого фактора.
            +1
            Если взять за практику такой порядок, когда объект сам прибирает за своими детьми, то потребность в ручных delete сократится почти до нуля. Но тем не менее даже в этом случае бывают очень странные утечки %).
            >Вообще есть автоматиеские указатели, которы разрушают объект, когда последний из указателей выходит за область видимости.
            Это если в памяти не висит какой-то объект, который вроде бы и в зоне видимости находится, но нафиг уже не нужен в общем то. Часто же и такое бывает. А тут ни какие анализаторы, вообще ничего не поможет, кроме владения ситуацией
        0
        Был уверен, что Debug-конфигурация проекта делает это самостоятельно. По крайней мере, в MSVS6.0 после смерти программы можно было посмотреть output и увидеть, кто где выделил и не собрал.

        А по поводу сборщиков мусора — фиг его знает, не определился: консервативные сборщики мусора — г##но в случае c с++, более агрессивные сканирующие — опасно, могут номер телефона в данных принять за ненужный указатель и «убрать».
          0
          Да, в VC6 утечки выводились автоматически, поскольку по умолчанию был установлен флаг _CRTDBG_LEAK_CHECK_DF. С семерки — нет, но его можно включить через _CrtSetDbgFlag(). Ну или явно использовать _CrtDumpMemoryLeaks() (в действительности, оно и звалось перед выходом из main() ).
          +4
          Valgrind вам в руки. Или, на худой конец, google heap checker.
            +1
            Google heap checker насколько я понял только под Linux. А вообще, насколько бы ни были хороши внешние инструменты, тут рассматривается несколько другая задача — можно назвать это самодиагностикой. Отдав программу на предрелизное тестирование, например, мы убедимся, что у программы нет серьезных утечек. А если вдруг есть, то и поймем — где они. Главное, тестеров не придется учить новым штукам. Для маленьких проектов это актуально.
              +4
              Обернуть запускальник в валгринд, и пусть тестируют полным циклом. Лог залить девам. Зачем учить новому?
                0
                полностью согласен!
            +2
            Аналогичными define-ами можно обернуть открытие/закрытие сокетов, файловых дескрипторов и еще разных ресурсов выделение. Но вообще валгринд отлично все ловит, заодно детектит некорректые чтения/записи, неициализированные данные, если, конечно программа с ним совместима.
              0
              В данном случае необходимость переопределения new имеет исторические корни, не в этом смысл. А Debug CRT тоже имеет средства обнаружения неициализированных данных (и обращений к уже освобожденным), выходов за границы массива. В сочетании с AppVerifier можно обрабатывать и попытки чтения/записи по неверным адресам. А вообще, Windows-программисты тоже должны как-то жить :)
                +1
                Да, валгринд то не работает под виндос. Специально посмотрел. Думаю для С++ виндос прогерам довольно занятный пост.
                  –1
                  для винды есть С№
                  С++ — для настоящих мужчин!
                    0
                    я все меньше и меньше встречаю приложений на С++ под винду
                0
                dmalloc, valgrind под Linux
                  0
                  «Dmalloc is not as good with C++ as C...» из их онлайн документации. И объясните тогда пожалуйста, чем dmalloc «лучше» debug CRT?
                  • НЛО прилетело и опубликовало эту надпись здесь
                      0
                      Можно дебагать уже собранные поделия. Но вообще это все фигня, валгринд рулит :)
                    0
                    а с чего вы взяли, что www.hpl.hp.com/personal/Hans_Boehm/gc/ снижает производительность?
                    судя по этому посту max630.livejournal.com/131587.html и коду, приведенном в нем, который я сегодня затестил, boehm GC работает быстрее, чем ручные delete
                      +2
                      Ну это не совсем честное сравнение. Фактически, в том посте рассматривается ситуация, когда выделяется множество динамических объектов небольшого размера, а затем все они убиваются за раз.

                      В таких случаях вообще можно порекомендовать какой-нибудь boost::pool и не париться.

                      В общем же случае, конечно, сборщик мусора снижает производительность, потому что это некоторый нетривиальный алгоритм, требующий времени на выполнение, я так понимаю. А хуже всего то, что он может вызвать неравномерность выполнения: 30 секунд полёт нормальный, потом он решил почистить память — и 30 секунд ещё висим.
                        –1
                        Субъективно очень даже снижает. Вообще надо бы взяться и оттестить, а то тезис
                        >Он, оказывается, работает быстрее, чем ручные delete. Раза в полтора.
                        немного удивляет и, быть может, подвох в чем-то другом.
                        –2
                        статья «люди, не надо писать правильно на с++ и придумывать „костыли/велосипеды“! Используйте true дебаг тулзы!»
                        • НЛО прилетело и опубликовало эту надпись здесь
                            0
                            могу обе руки дать, что в моем коде они есть. Специально оставил, что в будущем «покопаться» :)
                              –2
                              читаем саттера и учимся писать без ашипок…
                                –3
                                это я про то, что (цитата из статьи которую комментируем)
                                Не допускать ситуации вроде бы и не трудно — воспользуемся правилом «класть на место всё что взяли», но на практике это сильно осложняется человеческим фактором (банальная невнимательность), хитростью архитектуры и нелинейным порядком выполнения операторов, например, из-за применения исключений.

                                это не только не трудно, но большей частью абсолютно не накладно для рабочего кода. А уж исключения то просто обязаны заставлять вас писать ПРАВИЛЬНО, а не использовать дебаг тулзы для поиска мест «где же я еще прокосячил»
                                • НЛО прилетело и опубликовало эту надпись здесь
                                    +2
                                    я лишь заметил общий тон статьи, а не самоувернно заявлял что я пишу без ошибок. Не надо читать между строк особенно того что я не писал вообще.
                            0
                            Должен, однако, предупредить, что _CrtDumpMemoryLeaks может и врать. В проекте, над которым я сейчас работаю, он выдаёт дамп в сотни тысяч строк, тогда как сторонний полноценный профайлер выдаёт гораздо меньше утечек, и те большей частью в реализации строк в MFC/ATL. При этом отмечу, что студия не находит место выделения памяти в исходниках при клике на строках дампа, созданного функцией _CrtDumpMemoryLeaks.
                              0
                              _CrtDumpMemoryLeaks может врать только в одном ключе — он пропускает объекты выделенные вручную через HeapAlloc, GlobalAlloc. Как отсечь глобальные и статические объекты я написал, и да, их использованием MFC, увы, славится. Строку и имя файла определяет, если переопределить new, но «прыгать» по ней не будет. Но и время анализа и исправления утечки обычно больше, чем навигации по коду, всё-таки.
                                +1
                                Да нет, врать он и по-другому умеет — сообщает о несуществующих утечках, и никакой ссылки на место в исходниках не выдаёт. просто пишет «утекло столько-то байтов» и дампит кусочек памяти. Как раз отсутствие таких ссылок и является одной из причин считать эти утечки ложными. Другая причина — сторонний профайлер ничего о них не сообщает.
                              0
                              Всегда пользовался AQTime'ом, гораздо проще и пока ни разу не подводил. И ему можно верить, сервер работает месяцами — никаких ликов не обнаружено.
                                0
                                Недопонял, как эту фишку можно поюзать, скажем, в готовом проекте, во время выполнения которого все время в памяти находится большое число связанных объектов (например, 3д игрушка с физикой — там будет куча моделек, текстур, сущностей, связанных между собой и взаимодействующих с их физическими моделями). Я так понял, что эта фича позволяет снять дамп памяти, в котором каждый объект несет информацию о том, где он был аллоцирован. Но как это поможет, если мы не в состоянии отделить зерна от плевел, т.е. объекты, которые должны быть живы, от тех, которые должны были быть уничтожены на момент снятия дампа?
                                  +1
                                  Это реализуется при помощи _CrtMemCheckpoint, _CrtMemDumpAllObjectsSince (про это было) и _CrtMemDifference, про которую не упомянул, наверное зря.

                                  А вообще идеология простая, перед открытием проекта/сцены/модели делаем _CrtMemCheckpoint, после закрытия — _CrtMemDumpAllObjectsSince. Все новосозданные и не удаленные объекты являются подозрительными на утечку (но не обязаны ей быть).
                                    +1
                                    Ага, я невнимателен, спасибо.
                                    //
                                    Жаль только, что можно работать только с одним, глобальным пулом памяти. Было бы здорово научиться выделять для различных групп объектов различные пулы памяти (прописывать имена пулов в классах), и затем сравнивать состояния в контрольных точках конкретных пулов. Это, мне кажется, упростило бы локализацию утечек.
                                      0
                                      И это тоже возможно средствами debug CRT, за счет более хитрого переопределения new. От способа веет «велосипедностью», и я боюсь, что любители внешних анализаторов совсем разозлятся — но, если кому-то интересно, могу рассказать, как это делается.
                                        0
                                        Расскажите в двух словах без деталей, если не трудно.
                                        Мне приходит в голову либо добавить в структуру, оборачивающую выделенную память, свое собственное поле, в котором хранить ID пула, и задать макрос new для каждой группы классов, которые будут использовать этот пул. Это навряд ли получится, так как структура внутренняя.
                                        Либо переопределить глобальный оператор new и уже с указателем, полученным из alloc'a, связать информацию о пуле. Соответственно, при выводе информации об утечках нужно каким-то способом прицепить туда эту информацию, выводя статистику по нужным пулам, или группируя.
                                  +1
                                  ответ по птатформе: это Вин32
                                  для никс платформы что либо лучшее валгринда найти тяжело
                                  для макоси — есть cвой анализатор, для солярки свой (признаюсь с соляркой не работал, но на Конференции SunTechDays долго общался на тему утечек с разработчиками OpenSolaris)

                                  но если подойти к программированию теоретически, то есть пара простых правил по работе с выделением памяти:
                                  выделение ресурса, в том числе памяти — делается в конструкторе, освобождение в деструкторе.
                                  Используем «умные» (интеллектуальные) указатели.
                                  если соблюдаем правила, то, как правило, утечек не бывает. Хотя, пройтись валгриндом по приложению — святое дело.
                                    0
                                    Под макосью, кстати, валгринд работает, но отладчик аттачить не умеет, к сожалению :(
                                    –2
                                    с ними (утечками) не надо бороться
                                    их надо — не допускать!
                                      +1
                                      Вы, видимо, прочитали только заголовок.
                                      0
                                      да нет, сама статья мне понравилась тоже
                                      просто утечки надо не допускать, не знаю почему такая нервная реакция у минусеров :)
                                        0
                                        Мне кажется, что лучше бы сделали обзор всевозможных утилит и фреймворков для нахождения и локализации утечек памяти. CRT'шными функциями пользоваться умеют почти все, а для тех, кто ещё не умеет, всё понятно расписанно в мсдне. Да и CRT это однобоко в сторону винды, а как же быть тем, кто пишет под BSD например?
                                          0
                                          это верно,
                                          нам остается использовать BSD-шные утилиты,
                                          это было в моих комментах, за что и минуснули.

                                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                        Самое читаемое