Все новости

"Тихая охота": поиск утечек памяти

Нынче даже далёкие от IT профессий игроки знают термин "утечка памяти", который часто ассоциируется с "тормозами" или даже падениями игры. Но что такое эта самая "утечка памяти", от чего она возникает и как с ней бороться? Время для Ужасных Аналогий с Excel!

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

Рис. 1: Объект Амири (строка 1) ссылается на объект Огромный Меч (строка 5) из ячейки D1

Рис. 1: Объект Амири (строка 1) ссылается на объект Огромный Меч (строка 5) из ячейки D1

В какой-то момент объект может стать ненужным, и мы бы хотели освободить память, которую он занимает, очистив его строку. В языках без сборки мусора, таких как C++, это надо делать вручную - в какой-то момент кто-то должен сказать "эта строка больше не нужна". В таких языках причиной утечек памяти часто является то, что программист по каким-то причинам забывает это сделать, и объект навсегда остаётся занимать этот участок памяти. Если таких забытых объектов будет достаточно много, в какой-то момент память у нас кончится (представим себе, что у нас Excel 97 и в нём всего 65536 строк!), и игра упадёт при попытке найти место для очередного персонажа или предмета.

С другой стороны, если объект будет удалён, когда он ещё кому-то нужен (на него ссылается другая строка), то игра тоже может упасть, когда попробует по этой ссылке достать Огромный Меч, а обнаружит там, в лучшем случае, пустое место, а в худшем - вообще какого-нибудь гоблина.

Рис. 2: Объект Амири теперь ссылается на Нок-Нока и попытается использовать его в качестве оружия. К сожалению, компьютер чуть более строг, чем ваш ДМ за столом, и у неё это не получится

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

Однако, в языках, подобных C#, на котором написана наша игра, всё обстоит немного по другому, одновременно сложнее и проще.

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

Рис. 3: Амири выкинула Чёртов Огромный Меч и теперь пользуется Огненным Двуручником +1, а Огромный Меч из таблицы удалён и в строку 5 можно записать что-нибудь другое.

Но, как говорил старина Хайнлайн, “дарзанебы”! За такое удобство приходится платить тем, что на то время, пока Сборщик Мусора исследует пристальным взглядом табличку, игра висит. Хорошо, если у него на это уйдёт пара миллисекунд, которых игрок не заметит, да вот только табличка может дорасти до очень и очень больших размеров, что приводит к тому, что работа Сборщика Мусора может стать неприятно наглядной. Поэтому он берётся за неё не каждый кадр, а лишь периодически, например в тот момент, когда в таблице закончились свободные строчки (после того, как он по ней пройдётся, они появятся, и можно будет создать новый объект, и скорее всего не один).

Острый умом читатель заметит, что в нашей табличке на Амири никто не ссылается, и вообще говоря, Сборщик Мусора должен был бы удалить её объект. Это, в некотором роде, проблема курицы и яйца - где-то должен быть объект или объекты, на которые никто не ссылается, но которые нельзя удалять, иначе табличка станет пустой! Такие объекты в системах со сборкой мусора называются "корнями" и помечаются так, чтобы Сборщик Мусора их не удалял. Для простоты, будем далее считать, что первая строчка в нашей табличке - корень.

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

Рис. 4: Амири больше не ссылается на Огромный Меч, но ссылается на Квест Номер 5 (ячейка I1), который провален и (зачем-то) ссылается на Огромный Меч. Мы должны бы были удалить этот квест из памяти Амири, но забыли.

Сборщики Мусора бывают разные, и конкретно тот, который используется в Unity (на самом деле, в платформе Mono, которую использует Unity), отличается одной зловредной особенностью: удаляя ненужный более объект, он никому об этом не сообщает, и заставить его раскрыть информацию об этом событии невозможно. Это сильно затрудняет поиск утечек памяти по описанному выше методу: мы имеем момент создания объекта, но не имеем момента удаления, а значит не понимаем, какие объекты живы в каждый конкретный момент времени.

Unity имеет встроенные инструменты для анализа памяти, которые используют другой подход. Они позволяют составить и сохранить список живых объектов на данный момент. Можно просто посмотреть на этот список и поискать, нет ли там лишнего. Но, кроме того, часто бывает полезно сравнить эти списки в два разных момента времени, например, до загрузки сохранённой игры в меню и после возвращения обратно в меню из загруженной игры: игра могла не освободить все объекты, которые в меню уже явно не нужны, и мы их в этом случае увидим.

К сожалению, составление такого списка - довольно длительная операция, особенно в Unity 2018 (в Unity 2019 её сильно ускорили, но поменять версию Unity для Pathfinder: Kingmaker без больших затрат времени уже невозможно). "Длительная" в данном случае сильное преуменьшение - в некоторых случаях, она может занять более 10 часов, но даже в Unity 2019 её невозможно проделывать каждый кадр.

Поэтому мы решили пойти другим путём, и всё-таки реализовать некоторое подобие первого метода поиска утечек, описанного выше. Для этого, пришлось по локоть залезть в недра Mono и Unity, но зато в результате нам удалось сделать инструмент, который может показывать потребление памяти игрой в реальном времени (более-менее: честно говоря, когда происходит сборка мусора, игра ОЧЕНЬ сильно тормозит, но это, всё-таки, терпимо, и происходит только если запущен наш инструмент - игрокам замедления бояться не надо). Для этого мы делаем две вещи:

1) Каждый раз, когда создаётся новый объект (новая строчка в таблице), мы запоминаем номер этой строчки и тип объекта, который был создан

2) Каждый раз, когда происходит сборка мусора, сразу после неё, мы, фактически, её повторяем, но нашим собственным кодом, который проходится по всем запомненным в пункте 1 строчкам, и удаляет из этого списка все, на которые больше никто не ссылается.

По сути, мы делаем лишнюю работу только ради того, чтобы получить сообщение о том, что объект (строчка) был удалён, но поскольку это единственный способ получить эту информацию, приходится делать именно так.

Рис. 5: Созданный инструмент позволяет видеть график потребления памяти на протяжении всей работы игры, и исследовать любой временной отрезок, без необходимости составлять списки объектов в конкретные моменты

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

Если среди наших читателей есть Unity-разработчики, профессионалы или энтузиасты, которым подобный инструмент может быть полезен, то его исходный код и собранные версии для Windows можно скачать на нашем новом разделе на Github!

Максим Савенков, программист Owlcat Games

:
Все новости

Sign In

Create your account

Sign Up

Please, enter the name.
The password confirmation does not match.

I have an account