c++ - задачи - Утечки памяти-ужас каждого программиста?



для memory-leaks (8)

Я программирую игровой движок на C ++, который также поддерживает Lua.

Мой самый большой ужас: утечки памяти.

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

Я боюсь их, потому что они кажутся мне чрезвычайно сложными. Особенно в сложных системах. Если мой движок почти закончен, игра запускается и память уходит, что я буду делать? Где я начну искать?

  • Оправдан ли мой страх утечки памяти?
  • Как узнать, где находится утечка памяти?
  • Разве нет хороших инструментов, которые помогают найти источник утечек памяти сегодня?

https://ffff65535.com


Как узнать, где находится утечка памяти?

Valgrind


Как узнать, где находится утечка памяти?

Visual Leak Detector для Visual C ++ 2008/2010


Оправдан ли мой страх утечки памяти?

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

  • Выделить в стек
  • Используйте стандартные контейнеры (C ++ Standard Library и / или Boost), например, вместо написания собственного связанного списка, используйте std :: list
  • В связи с вышеизложенным, сохраняйте объекты по значению, если можете (то есть, если создание копии не слишком дорого), или, по крайней мере, ссылки (со ссылками, о которых вам не нужно беспокоиться о нулевых значениях) для размещения выделенных объектов
  • Передайте ссылки на объекты стека, где это возможно
  • Когда вам нужно выделить свою собственную память, попробуйте использовать RAII для выделения в конструкторе и освободить его снова в деструкторе. Убедитесь, что этот объект расположен в стеке.
  • Когда вам нужно использовать указатели для распределения данных вручную, используйте умные указатели для автоматического освобождения объектов, которые больше не используются
  • Четко определите, каким объектам принадлежит какая память. Постарайтесь ограничить вашу иерархию классов как можно меньшим количеством владельцев ресурсов и позволить им распределять и освобождать объекты для вас.
  • Если вам нужен более общий доступ к динамической памяти, напишите менеджер ресурсов, который станет владельцем объектов. Система дескрипторов, как описано здесь , также полезна, поскольку она позволяет вам «собирать мусор» в памяти, которая больше не нужна. Дескрипторы также могут быть помечены тем, какой подсистеме принадлежит какая память, поэтому вы можете вывести состояние использования системной памяти, например, «подсистеме А принадлежит 32% выделенной памяти»
  • Операторы перегрузки новые и удаляемые для выделенных классов, чтобы вы могли поддерживать дополнительные метаданные о том, кто / что / где / когда выделил, что

Как узнать, где находится утечка памяти?

Набор инструментов Valgrind: Memcheck, Cachegrind, Callgrind, Massif, Helgrind ...

Вы также можете попробовать компилировать с электрическим забором (-lefence для gcc) или вашими компиляторами. Вы также можете попробовать набор инструментов Intel, особенно если вы пишете многопоточный код или код, чувствительный к производительности (например, Parallel Studio), хотя они и дороги.

Разве нет хороших инструментов, которые помогают найти источник утечек памяти сегодня?

Конечно, есть. Смотри выше.

Теперь, так как вы пишете игру, я поделюсь некоторыми своими мыслями / опытом по разработке игр:

  • Предварительно распределите как можно больше (в идеале все) в начале каждого уровня. Тогда во время уровня вам не нужно беспокоиться об утечках памяти. Сохраняйте указатели на все, что вы выделили, и в начале следующего уровня освобождайте их все, так как не должно быть возможности существования висячих указателей, когда следующий уровень загружает свой собственный чистый набор данных.
  • Используйте распределители стека (либо используя реальный стек, либо создавая свой собственный в куче) для управления распределениями в пределах уровня или фрейма. Когда вам нужна память, вытолкните ее из верхней части стека. Затем, когда этот уровень или фрейм будет завершен, вы можете просто очистить стек (если вы храните только типы POD, это быстро и просто: просто сбросьте указатель стека. Я использую это для размещения сообщений или системы обмена сообщениями в моем собственном игровом движке. : во время кадра сообщения (типы POD фиксированного размера в моем движке) выделяются из стекового пула памяти (чья память предварительно выделена). Все события просто извлекаются из стека. В конце кадра я «поменять» стек на второй (так, чтобы обработчики событий могли также отправлять события) и вызвать каждый обработчик для событий. Наконец, я просто сбрасываю указатель. Это делает распределение сообщений очень быстрым и невозможным для утечки памяти.
  • Используйте систему ресурсов (с дескрипторами) для всех ресурсов, которыми вы не можете управлять через пулы памяти или распределители стека: буферы, данные уровня, изображения, аудио. Моя система ресурсов, например, также поддерживает потоковые ресурсы в фоновом режиме. Дескрипторы создаются немедленно, но помечаются как «не готовы», пока ресурс не завершил потоковую передачу.
  • Создайте свои данные, а не код (то есть сначала разработайте структуры данных). Изолируйте распределение памяти, где это возможно.
  • Попробуйте сохранить похожие данные вместе . Это не только облегчит управление его временем жизни, но также может улучшить производительность ваших движков за счет лучшего использования кэша (например, все позиции символов хранятся в контейнере позиций, все позиции обновляются / обрабатываются вместе и т. Д.).
  • Наконец, если вы можете программировать в максимально возможном чисто функциональном стиле, вместо того, чтобы слишком полагаться на ООП, вы можете упростить ряд проблем: управление памятью проще, потому что части кода могут изменять данные. Распределение происходит до вызова функций, освобождение - когда они завершены (конвейер потока вызовов функций). Во-вторых, если вы имеете дело с чисто функциональным кодом, работающим с неизменяемыми данными, многоядерное программирование будет значительно упрощено. Двойная победа. Например, в моем движке данные игровых объектов обрабатываются чисто функциональным кодом, который принимает в качестве входных данных текущее состояние игры и возвращает в качестве выходных данных игровое состояние следующих кадров. Это позволяет очень легко отслеживать, какие части кода могут выделять или освобождать память, и, как правило, отслеживать время жизни объектов. Из-за этого я могу параллельно обрабатывать игровые объекты.

Надеюсь, что это помогло.


AFAIK Valgrid - это только Linux.
Для Windows у вас есть такие инструменты, как BoundsChecker и Purify .
Если вы используете Visual Studio, библиотека C Runtime (CRT) также предоставляет удивительно простой и полезный инструмент для поиска утечек памяти из коробки. Прочитайте о _CrtDumpMemoryLeaks и связанных с ним функциях и макросах.
Он в основном позволяет получить индексированный дамп утечек памяти при выходе из процесса, а затем позволяет установить точку останова во время, когда утечка памяти была выделена, чтобы точно определить, когда это произошло. Это в отличие от большинства других инструментов, которые дают вам только посмертный анализ без способа воспроизвести события, которые привели к утечке памяти.
Использование этих маленьких драгоценных камней с первого дня дает вам относительное спокойствие, что вы в хорошей форме.


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


Необработанные указатели - только одна потенциальная причина утечек памяти.

Даже если вы используете умные указатели, такие как shared_ptr, вы можете получить утечку, если у вас есть цикл - лекарство от этого - где-то использовать слабый_птр, чтобы разорвать цикл. Использование умных указателей не является панацеей от утечек памяти.

Вы также можете забыть о виртуальном деструкторе в базовом классе и получить утечки таким образом.

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

Такие инструменты, как valgrind, очень и очень полезны для поиска утечек, но они не всегда сообщают вам, где должно быть исправление (например, в случае циклов или объектов, удерживающих умные указатели).


Существуют различные методы для отслеживания утечек памяти.

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

Если вы используете предварительно скомпилированные контейнеры все время, это не сработает, так как все выделения будут в контейнерах. Тогда ваши варианты:

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

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

Также см. Статью Electronic Arts STL C ++ для обсуждения того, что нужно сделать в STL / C ++ для поддержки правильной разработки игр. (Вероятно, он немного более хардкорный, чем ваш движок, но он, безусловно, содержит много самородков вдохновения и изобретательности.)


Требуется четко определенная модель «времени жизни объекта». Каждый раз, когда вы делаете «новый», вы должны думать о

  1. Кому принадлежит этот объект кучи? т.е. кто отвечает за поддержание этого указателя и разрешение другим "клиентам" ссылаться на него?

  2. Кто несет ответственность за удаление этого объекта? Обычно это № 1, но не обязательно.

  3. Есть ли клиенты этого объекта, чье время жизни больше, чем у этого объекта? Если это так, и они на самом деле хранят этот указатель кучи, он разыменовывает память, которая больше не «существует». Вам может потребоваться добавить некоторые механизмы уведомления или изменить дизайн модели «время жизни объекта».

Много раз вы исправляете утечки памяти, но затем сталкиваетесь с проблемой № 3. Вот почему лучше продумать модель времени жизни вашего объекта, прежде чем писать слишком много кода.





lua