Загрузка...
 
Печать
ИГРОКОДИНГ  »  ИГРОКОДИНГ: Учебный курс  »  Знакомство с XNA Framework и XNA Game Studio  »  XNA - Управление памятью

XNA Game Studio: Управление памятью




В .NET Framework существуют два основных типа объектов, связанных с управлением паматью - ссылка (reference) и занчение (value).1
  • Примеры типов со значениями (value types): энумерации (enum), исчисляемые типы (integrals: byte, short, int, long), числа с плавающей точкой (floating types: single, double, float), элементарные (primitive) типы (bool, char) и структуры (structs). Типы со значениями хранят свои данные в стеке (stack) текущего потока (thread).
  • Примеры объектов, представляющих собой ссылочные типы (reference types): массивы (arrays), исключения (exceptions), аттрибуты (свойства, attributes), делегаты (delegates) и классы (classes). Ссылочные типы хранят свои данные в т.н. "уравляемой куче" (managed heap).
Когда по умолчанию мы передаём переменные в методы, мы на самом деле передаём их текущие значения. Для типов со значениями это означает, что на самом деле копируем данные в стек. Поэтому всё, что мы делаем со скопированным значением внутри метода, никак не влияет на исходную переменную и её значение.
Когда мы передаём в метод объект ссылочного типа, мы на самом деле передаём в него ссылку на этот объект. Исходные данные объекта при этом не копируются, передаётся лишь адрес в памяти, указывающий на них. Вот почему мы должны передавать большие переменные со значимыми типами путём передачи ссылки на них, а не копируя значение (где это возможно). Операция копирования адреса выполняется намного быстрее, нежели копирование всего значения целиком.
Для передачи ссылок на объекты в методы мы используем ключевое слово ref. Его не следует применять при передачи в методы объектов ссылочных типов, т.к. это выполняется заметно медленнее. Но для типов со значениями (например структуры), содержащие большие объёмы данных, ключевле слово ref - это то, что нужно.
Важно понимать, что если даже у нас есть объект ссылочного типа и мы передаём его в метод, который принимает данные объектного типа, и мы упаковываем (box) объект (явно или неявно), то в этом случае в методе создаётся копия даных, которые, в принципе, уже существовали до этого, а не ссылка в памяти на них. Возьмём, к примеру, класс, который принимает данные типа объект (object) (например метод Add класса ArrayList). Мы передаём объект ссылочного типа, но, вместо того, чтобы передать в класс ссылку, внутри него создаётся новая копия этого объекта и уже затем ссылка на эту копию передаётся в метод. Это крайне неэффективно, вызывает перерасход памяти и приводит к более частым вызовам сборщика мусора (garbage collector). Для избежания подобных ситуаций необходимо использовать вещественные типы данных везде, где это возможно.

Сборщик мусора (Garbage collector)

Огромным плюсом использования управляемого кода является тот факт, что нам не нужно беспокоиться об утечках памяти (memory leaks) например в случае утери указателей на ссылочный сегмент памяти (referenced memory). Конечно, даже в управляемом коде утечек памяти избежать вряд ли удасться, но здесь нам всегда придёт на помощь сборщик мусора. Наиболее часто утечки памяти происходят, когда у нас есть ссылка (handle) на указатель(pointer), который по каким то причинам оказался обнулён (равен null). Она будет оставаться в памяти до момента выхода из игры.

Использование выражения using

Выражения using можно без труда найти в начале файла Game1.cs шаблонного приложения XNA Game Studio:
Фрагмент файла Game1.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;
...

Точка входа в приложение применяет данное выражение при создании игрового объекта (game object) и затем для вызова его метода Run. Выражение using эффективно размещает блок try/finally вокруг тестируемого кода, вместе с тем помещая вызов метода Dispose внутри секции finally.

Сборщик мусора в .NET Framework (Windows)

Всякий раз при создании объекта (с использованием слова new), он размещается в т.н. управляемой куче (managed heap). После этого .NET рассчитывает объём памяти, необходимый для объекта и подтверждает (confirms), что в управляемой куче такой объём памяти найдётся и будет предоставлен. Далее вызывается конструктор объекта, после чего выполняемый (executing) код возвращает ссылку на этот объект (его координаты в управляемой куче). Память выделяется последовательно (in contiguous manner), т.е. объекты обычно хранятся один за другим, в порядке их создания.
В случае, когда выделение памяти в управляемой куче невозможно, вызывается сборщик мусора, который освобождает всю неиспользуемую память. При этом он делает ряд предположений (assumptions), одним из которых является предположение о том, что все создаваемые объекты будут существовать в памяти недолго. Другое предположение гласит, что, если в памяти присутствуют объекты, просуществовавшие какое-то время (a while), они продолжат присутствовать в памяти ещё какое-то время. Из-за этих предположений сборщик мусора в разных версиях .NET Framework работает по-разному. Всего существует 3 т.н. "генерации" (generations, режима, "колец" защиты) сборщика мусора .NET Framework. .NET Compact Framework, работающий на Xbox 360, имеет лишь одну генерацию. Первая генерация (0) сохраняет в памяти все недавно добавленные данные, т.е. память выделяется динамически, по мере добавления новых переменных. Затем, когда переменные выходят за пределы "видимости" (out of scope), память помечается как неактивная (inactive), явно обнуляется и т.д. Помечая память как неактивную, .NET на самом деле устанавливает корневой заголовок данных (root; который на самом деле яаляется обычным указателем на опеределённое место в памяти), в null. При таком раскладе, спустя какое-то время, управляемая куча оказывается переполненной. Когда это происходит, вызывается сборщик мусора и отправляет все активные объекты (корни которых не равны нулю) в генерацию 1 (generation 1), освобождая всё пространство памяти, ранее занятое генерацией 0. Когда переполняется генерация 1, с ней происходит то же самое, что и с генерацией 0. При этом все активные объекты отправляются в генерацию 2 (generation 2). Когда эта последняя генерация также оказывается переполненной, происходит полный сбор "мусора" (full garbage collection), что требует значительных вычислительных ресурсов, т.е. "дорого" с точки зрения вычислительных мощностей. Так, если объект достаточно большой, сборщик мусора пропускает генерацию 0 и переходит прямо к генерации 2, ни капли не заботясь о потере производительности, по максимуму освобождает память и повторяет весь процесс, начиная с генерации 1.
Так какое же отношение имеет всё вышеописанное к написанию комп. игр для Windows? Игрокодеру следует обращать внимание на то как и в какой момент он создаёт объекты. Не следует создавать чрезмерно большие объекты (размер игровой карты первой Half-Life не мог превышать 3 Мб), и создаваемые объекты должны иметь минимально возможный срок жизни, чтобы они не перешли в генерацию 1. Также мы должны своевременно удалять более неиспользуемые объекты. Мы должны создавать объекты, которые тесно взаимосвязаны друг с другом, чтобы таким образом они могли вместе проходить генерации сборщика мусора. Мы также должны быть осторожны и не ассоциировать маложивущие объекты (short-lived objects) с долгоживущими (long-lived), т.к. долгоживущие объекты могут сохранять ссылки на маложивущие, что приведёт к тому, что последние могут оказаться недоступными. Маложивущие объекты, требующие небольшие объёмы памяти, не приводят к снижению производительности. Долгоживущие объекты (до тех пор, пока они остаются не слишком большими, также не приводят к снижению производительности. Если долгоживущие объекты слишком велики, это рано или поздно вызовет генерацию 2, означающую полное заполнение всей доступной памяти, из-за чего периодичность вызова "глобального" сборщика мусора заметно возрастёт. Необходимо поддерживать (keep) минимальные размеры объектов. Существуют также объекты со средней продолжительностью жизни (midlife objects). Они могут провоцировать вызов генерации 1 и затем стать неактивными. В принципе это не особо большая проблема. Большой проблемой это становится, когда такие объекты продолжают жить и переходят в генерацию 2, вскоре исчезнув. Позволяя объекту "умереть" в генерации 2, вместо генерации 1, мы сильно теряем в эффективности использования ресурсов компьютера, т.к. когда сборщик мусора производит полную очистку (собирает данные в генерации 2), он обязан заглянуть в каждый из объектов в управляемой куче для определения, жив объект или нет. Во время такой "инспекции" сильно нагружается процессор, что создаёт эффект "бутылочного горла", замедляя все остальные вычисления игрового приложения. В конце концов любой объект так или иначе влияет на общую производительность приложения. Мы должны избегать вызова полных зачисток памяти.

Источники:


1. Chad Carter. Microsoft XNA Game Studio 3.0 Unleashed. - Sams Publishing. 2009


ИГРОКОДИНГ  »  ИГРОКОДИНГ: Учебный курс  »  Знакомство с XNA Framework и XNA Game Studio  »  XNA - Управление памятью

Contributors to this page: slymentat .
Последнее изменение страницы Суббота 02 / Март, 2019 22:58:19 MSK автор slymentat.

Помочь проекту

Яндекс-деньги: 410011791055108