При создании объекта, строки или массива, память для его хранения выделяется из центрального пула, который называется куча (heap). Когда использование элемента прекращается, память, которую он занимал, можно будет освободить и использовать для чего-нибудь ещё. В прошлом, выделение и освобождение этих блоков памяти с помощью вызовов соответствующих методов в основном лежало на плечах программистов. Теперь за вас памятью автоматически управляют среды выполнения, например, движок Mono у Unity. Автоматическое управление памятью требует меньше усилий при написании кода, чем прямое выделение / освобождение и значительно уменьшает потенциал для утечки памяти (ситуации, когда память была выделена, но впоследствии так и не была освобождена).
При вызове функции, значения её параметров копируются в зону памяти, зарезервированную специально для этого вызова. Типы данных, которые занимают всего лишь несколько байт могут быть скопированы легко и быстро. Однако, обычно объекты, строки и массивы гораздо больше, и было бы очень неэффективно копировать эти данные как обычно. К счастью, это не обязательно; реальное место хранения для больших элементов выделяется из кучи и для запоминания его местоположения используется небольшое “указательное” значение. После этого, во время передачи параметра нужно будет скопировать только указатель. Пока система среды выполнения может найти определяемый указателем элемент, можно использовать одиночную копию данных так часто, как это требуется.
Типы, которые хранятся напрямую и копируются при передаче параметра, называются значимыми типами (value types). В них включены integer, float, boolean и структурные типы Unity (например, Color и Vector3). Типы, которые выделяются из кучи, после чего доступ к ним получается при помощи указателя, называются ссылочными типами, т.к. значения хранящиеся в переменной только “ссылаются” на реальные данные. Примеры ссылочных типов включают объекты, строки и переменные.
Менеджер памяти отслеживает зоны в куче, которые определены как неиспользуемые. При запросе нового блока памяти (допустим, при создании экземпляра объекта), менеджер выбирает неиспользуемую зону, из которой следует выделить блок, и затем удаляет выделенную память из зоны известного неиспользуемого пространства. Последующие запросы обрабатываются тем же способом, пока в неиспользуемой зоне будет достаточно места для выделения блока необходимого размера. Доступ к ссылочному элементу в куче может быть получен только пока есть ссылочные переменные, которые могут его найти. Если все ссылки к блоку памяти пропадут (т.е. ссылочные переменные были переназначены или они являются локальными переменными, которые теперь вне контекста), то занимаемая им память может быть ещё раз безопасно выделена.
Чтобы определить, какие блоки кучи больше не используются, менеджер памяти просматривает все активные ссылочные переменные и отмечает блоки, к которым они ссылаются как “live” (используемые). В конце поиска, любое пространство между используемыми блоками менеджером считается пустым и в будущем может быть использовано для выделения. По очевидным причинам, процесс обнаружения и освобождения неиспользуемой памяти известен как сборка мусора(Garbage Collection, или GC для сокращения).
Сборка мусора - это автоматический и невидимый программисту процесс, но процесс сборки на деле требует некоторого времени “закулисной” работы процессора. При правильном использовании, автоматическое управление памятью обычно не уступает по производительности ручному выделению. Тем не менее, для программиста важно избегать ошибок, которые будут вызывать сборку чаще чем надо и выражаться в задержках работы.
Есть несколько алгоритмов с сомнительной репутацией, которые могут быть ночным кошмаром для GC, хотя на первый взгляд они выглядят невинно. Постоянное объединение строк - классический пример:-
//C# script example
using UnityEngine;
using System.Collections;
public class ExampleScript : MonoBehaviour {
void ConcatExample(int[] intArray) {
string line = intArray[0].ToString();
for (i = 1; i < intArray.Length; i++) {
line += ", " + intArray[i].ToString();
}
return line;
}
}
//JS script example
function ConcatExample(intArray: int[]) {
var line = intArray[0].ToString();
for (i = 1; i < intArray.Length; i++) {
line += ", " + intArray[i].ToString();
}
return line;
}
Ключевой деталью является то, что новые части не добавляются к строке один за одним. На самом деле, в каждой итерации цикла предыдущее содержание переменной “умирает” - выделяется целая новая строка для размещения в ней оригинальной части и новой части в конце. Т.к. строка становится длиннее, то с увеличивающимся значением i, значение потребляемого пространства кучи также повышается и с лёгкостью достигает сотни байтов свободного пространства кучи при каждом вызове этой функции. Если вам нужно объединить много строк вместе, то более подходящим вариантом будет класс Mono библиотеки System.Text.StringBuilder.
Однако, даже повторяющееся соединение не вызовет много неприятностей, если не вызывать его часто, и в Unity под этим обычно подразумевается каждый кадр. Что-то вроде:-
//C# script example
using UnityEngine;
using System.Collections;
public class ExampleScript : MonoBehaviour {
public GUIText scoreBoard;
public int score;
void Update() {
string scoreText = "Score: " + score.ToString();
scoreBoard.text = scoreText;
}
}
//JS script example
var scoreBoard: GUIText;
var score: int;
function Update() {
var scoreText: String = "Score: " + score.ToString();
scoreBoard.text = scoreText;
}
…будет выделять новые строки при каждом вызове Update и генерировать постоянный поток нового мусора. Большую часть этого можно сохранить, обновляя текст только тогда, когда счёт меняется:-
//C# script example
using UnityEngine;
using System.Collections;
public class ExampleScript : MonoBehaviour {
public GUIText scoreBoard;
public string scoreText;
public int score;
public int oldScore;
void Update() {
if (score != oldScore) {
scoreText = "Score: " + score.ToString();
scoreBoard.text = scoreText;
oldScore = score;
}
}
}
//JS script example
var scoreBoard: GUIText;
var scoreText: String;
var score: int;
var oldScore: int;
function Update() {
if (score != oldScore) {
scoreText = "Score: " + score.ToString();
scoreBoard.text = scoreText;
oldScore = score;
}
}
Другая потенциальная проблема появляется, когда функция возвращает массив:-
//C# script example
using UnityEngine;
using System.Collections;
public class ExampleScript : MonoBehaviour {
float[] RandomList(int numElements) {
var result = new float[numElements];
for (int i = 0; i < numElements; i++) {
result[i] = Random.value;
}
return result;
}
}
//JS script example
function RandomList(numElements: int) {
var result = new float[numElements];
for (i = 0; i < numElements; i++) {
result[i] = Random.value;
}
return result;
}
Это очень элегантный и удобный тип функции, если создаётся новый массив заполненный значениями. Однако, если её постоянно вызывать, то каждый раз будет выделяться свежая память. Т.к. массивы могут быть очень большими, свободное пространство кучи может быть постоянно использовано, что приведёт к частой сборке мусора. Единственный способ избежать этой проблемы, это извлечь пользу из того факта, что массив - ссылочный тип. Массив, использованный в функции как параметр, может быть использован внутри этой функции и результаты останутся после возврата функции. Функция, вроде указанной выше, часто может быть замещена чем-нибудь вроде:-
//C# script example
using UnityEngine;
using System.Collections;
public class ExampleScript : MonoBehaviour {
void RandomList(float[] arrayToFill) {
for (int i = 0; i < arrayToFill.Length; i++) {
arrayToFill[i] = Random.value;
}
}
}
//JS script example
function RandomList(arrayToFill: float[]) {
for (i = 0; i < arrayToFill.Length; i++) {
arrayToFill[i] = Random.value;
}
}
Она просто замещает существующие данные массива новыми значениями. Хотя она и требует стартового выделения массива, чтобы быть выполненной в коде вызова (который выглядит не очень элегантно), функция не будет создавать какого-либо нового мусора, когда она будет вызвана.
Как упоминалось выше, лучше всего избегать выделений настолько, насколько возможно. Однако, учитывая, что полностью от них избавиться нельзя, есть 2 основные стратегии, которые вы можете использовать для минимизации их влияния на игровой процесс:-
Эта стратегия лучше всего подходит играм с долгими сессиями игрового процесса, когда стабильный FPS стоит на первом месте. Подобная игра обычно будет выделять небольшие блоки почаще, но эти блоки будут в использовании совсем не долго. Типичный размер кучи при использовании подобной стратегии на iOS составляет около 200 КБ, и сборка мусора занимает где-то 5 миллисекунд на iPhone 3G. Если размер кучи увеличить до 1 МБ, то сборка займёт где-то 7 миллисекунд. Следовательно, иногда будет разумно запрашивать сборку мусора раз в определённый интервал. В результате сборка будет проходить чаще чем строго необходимо, но сборка пройдёт быстрее с минимальным влиянием на игровой процесс:-
if (Time.frameCount % 30 == 0)
{
System.GC.Collect();
}
Однако вам следует использовать эту технику аккуратно и проверять статистку профайлера, чтобы убедиться, что это действительно уменьшает время сборки мусора для вашей игры.
Эта стратегия лучше всего подходит для игр, где выделения (и последующие сборки) памяти относительно редки и их можно провести во время пауз в игровом процессе. Таким образом кучу можно сделать максимально большой (но не на столько, чтобы перегрузить память системы, что может вызвать закрытие вашего приложения). Однако, среда запуска Mono по возможности избегает автоматического расширения кучи. Вы можете расширить кучу вручную путём предварительного выделения некоторого пространства во время запуска (т.е. вы вызываете “бесполезный” объект, который выделен только для влияния на менеджер памяти):-
//C# script example
using UnityEngine;
using System.Collections;
public class ExampleScript : MonoBehaviour {
void Start() {
var tmp = new System.Object[1024];
// make allocations in smaller blocks to avoid them to be treated in a special way, which is designed for large blocks
for (int i = 0; i < 1024; i++)
tmp[i] = new byte[1024];
// release reference
tmp = null;
}
}
//JS script example
function Start() {
var tmp = new System.Object[1024];
// make allocations in smaller blocks to avoid them to be treated in a special way, which is designed for large blocks
for (var i : int = 0; i < 1024; i++)
tmp[i] = new byte[1024];
// release reference
tmp = null;
}
Достаточно большую кучу не следует заполнять под завязку между этими паузами в игровом процессе, включающими в себя сборку. Когда такая пауза происходит, вы можете напрямую запросить сборку:-
System.GC.Collect();
Опять же, вам следует аккуратно использовать эту стратегию и обращать внимание на статистику профайлера, нежели просто предполагать, что желаемый эффект достигнут.
There are many cases where you can avoid generating garbage simply by reducing the number of objects that get created and destroyed. There are certain types of objects in games, such as projectiles, which may be encountered over and over again even though only a small number will ever be in play at once. In cases like this, it is often possible to reuse objects rather than destroy old ones and replace them with new ones.
Управление памятью - тонкая и сложная тема, к которой было приложено много академических усилий. Если вы заинтересованы в дальнейшем изучении этой темы, тогда рекомендуем посетить memorymanagement.org - отличный ресурс с большим количеством публикаций и онлайн статей. Больше информации о пулинге объектов можно найти на странице Wikipedia и на Sourcemaking.com.