Execution Order of Event Functions (Orden de Ejecución de Funciones de Evento)
Compilación Dependiente a la Plataforma

Entender la Gestión Automática de Memoria

Cuando un objeto, cadena o array es creado, la memoria requerida para almacenarlo se asigna desde un pool central llamada la pila. Cuando el ítem ya no está más en uso, la memoria que una vez ocupaba puede ser recuperada y usada para otras cosas más. En el pasado, era responsabilidad del programador el asignar y liberar estos bloques de memoria de la pila en forma explícita usando las llamadas de función apropiadas. Hoy en día, los sistemas en tiempo de ejecución como el motor Mono de Unity gestionan la memoria automáticamente. La gestión automática de memoria requiere menos esfuerzo de escritura de código que asigne/libera explícitamente, y reduce enormemente la posibilidad de una fuga de memoria (situación en donde la memoria es asignada, pero nunca es liberada después).

Tipos de valor y tipos de referencia

Cuando una función es llamada, los valores de sus parámetros son copiados a un área de la memoria que es reservada para este llamado en específico. Los tipos de datos que ocupan sólo unos pocos bytes pueden ser copiados en forma muy rápida y fácil. Sin embargo, es común que los objetos, cadenas y arrays sean muy grandes, y sería muy ineficiente si estos tipos de datos fueran copiados con regularidad. Afortunadamente, esto no es necesario; el verdadero espacio de almacenamiento para un ítem grande es asignado desde la pila y un pequeño valor de “apuntador” es usado para recordar su ubicación. A partir de entonces, sólo el apuntador necesita será copiado durante el paso de parámetros. Siempre que el sistema en tiempo de ejecución pueda localizar el ítem identificado por el apuntador, una copia sencilla de los datos puede ser usada tan a menudo como sea necesario.

Los tipos que son almacenados directamente y copiados durante el paso de parámetros son llamados tipos de valor (value types). Entre estos están los integers, floats, booleans y los tipos de estructuras de Unity (p.ej. Color y Vector3). Los tipos que son asignados en la pila y luego accesados por medio de un puntero son llamados tipos de referencia (reference types), dado que el valor almacenado en la variable sólo se “refiere” a los datos reales. Ejemplos de tipos de referencia son los objetos, las cadenas y los arrays.

Asignación y recolección de basura

El gestor de memoria realiza un seguimiento de las áreas de la pila que se sepa que no están siendo usadas. Cuando un nuevo bloque de memoria es solicitado (digamos, cuando un objeto es instanciado), el gestor escoge un área no usada a la que se le asigna el bloque y luego remueve la asignación de memoria en el espacio no usado conocido. Solicitudes posteriores son manejadas del mismo modo hasta que no hayan suficientes áreas libres y grandes para asignar el tamaño de bloque solicitado. Es altamente improbable en este punto que toda la memoria asignada de la pila esté todavía en uso. Un ítem por referencia en la pila únicamente puede ser accedido en la medida que todavía hayan variables por referencia que puedan localizarlo. Si todas las referencias a un bloque de memoria se han ido (es decir, las variables por referencia han sido reasignadas, o hay variables locales que están fuera de alcance) entonces la memoria que ocupan puede ser reasignada en forma segura.

Para determinar cuáles bloques de la pila no están más en uso, el gestor de memoria busca a través de todas las variables por referencia activas y marca los bloques que éstas están señalando como bloques “vivos”. Al final de la búsqueda, cualquier espacio entre los bloques vivos es considerado como vacío por el gestor de memoria y puede ser usado para posteriores asignaciones. Por motivos obvios, el proceso de localizar y liberar memoria no usada es conocido como recolección de basura (o abreviadamente, GC por “Garbage Collection”).

Optimización

La recolección de basura (Garbage collection) es automática e invisible para el programador; pero detrás de escena, el proceso de recolección requiere en realidad de un tiempo de CPU significativo. Cuando se usa correctamente, la gestión automática de memoria por lo general igualará o superará a la asignación manual en términos de rendimiento global. Sin embargo, es importante que el programador evite errores que accionen el recolector más frecuentemente de lo que es necesario, a fin de evitar interrupciones en la ejecución.

Hay algunos algoritmos famosos que son verdaderas pesadillas para el recolector, aunque parezcan inocentes a primera vista. Un ejemplo clásico es la concatenación repetida de cadenas:-

//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;
}

El detalle clave aquí es que los nuevos pedazos no están siendo agregados uno por uno a la cadena en el mismo sitio en que está. Lo que realmente ocurre es que cada vez que se repite el ciclo, el contenido previo de la variable line se marca como muerto, y una nueva cadena completa es asignada para que contenga el pedazo original más la nueva parte al final. Dado que la cadena se vuelve más larga a medida que el valor de i se incrementa, la suma del espacio en la pila (también conocido como espacio de almacenamiento dinámico) que está siendo consumido también se incrementa, por lo que fácilmente son empleados cientos de bytes de espacio libre en la pila cada vez que esta función es invocada. Si necesitas concatenar juntas muchas cadenas, una opción mucho mejor es la clase System.Text.StringBuilder de la librería de Mono.

Sin embargo, incluso la concatenación repetida no causará muchos problemas si no es llamada con frecuencia, y en Unity esto usualmente implica la actualización de frames. Algo como:-

//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;
}

…asignará nuevas cadenas cada vez que Update sea invocado, y generará una filtración constante de basura nueva. Gran parte de esto puede ser evitado actualizando el texto sólo cuando el puntaje cambie:-

//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;
    }
}

Otro problema potencial ocurre cuando una función devuelva un valor de array:-

//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;
}

Este tipo de función es muy elegante y conveniente cuando se crea un nuevo array que es ocupado con valores. No obstante, si es llamado repetidamente entonces va a ser asignado un nuevo espacio en la memoria en cada ocasión. Dado que los arrays pueden ser muy grandes, el espacio libre en la pila puede quedar utilizado rápidamente, resultando en frecuentes recolecciones de basura. Una forma de evitar este problema es hacer uso del hecho que un array es un tipo de referencia. Un array pasado a una función en forma de un parámetro puede ser modificado dentro de esta función, y el resultado permanecerá después que la función retorne y concluya. Una función como la de arriba con frecuencia puede ser reemplazado con algo como:-

//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;
    }
}

Lo que esto hace es sólo reemplazar el contenido existente del array con valores nuevos. Aunque esto requiere que la asignación inicial del array sea hecho en el código que invoca a la función (que no pareciera ser tan elegante), esta función no generará basura nueva cuando sea ejecutada.

Solicitar una Recolección

Como fue mencionado arriba, es mejor evitar asignaciones tanto como sea posible. Sin embargo, dado que no pueden ser completamente eliminadas, hay dos estrategias principales que puedes usar para minimizar su intrusión en la jugabilidad:-

Mantener la pila pequeña con recolecciones frecuentes

Esta estrategia es usualmente mejor para juegos que tengan periodos largos de juego en donde la velocidad de cuadros sea la preocupación principal. Un juego de este tipo típicamente asigna bloques pequeños con frecuencia, pero estos bloques sólo estarán en uso brevemente. El tamaño típico de la pila al usar esta estrategia en iOS es alrededor de los 200KB y la recolección de basura tomará unos 5ms en un iPhone 3G. Si la pila se incrementa a 1MB, la recolección tomará unos 7ms. Por tanto, puede ser ventajoso en ocasiones solicitar una recolección de basura a un intervalo de frames regular. Esto por lo general hace que las recolecciones ocurran más frecuente de lo que estrictamente necesario, pero serán procesadas más rápido y con un efecto mínimo sobre la jugabilidad:-

if (Time.frameCount % 30 == 0)
{
   System.GC.Collect();
}

Sin embargo, debes usar esta técnica con precaución y verificar las estadísticas del profiler para asegurarte que realmente se está reduciendo el tiempo de recolección para tu juego.

Dejar grande la pila con recolecciones lentas y poco frecuentes

Esta estrategia funciona mejor en juegos donde las asignaciones (y por tanto, las recolecciones) sean relativamente poco frecuentes y puedan ser manejadas cuando hayan pausas en el ritmo del juego. Es útil que la pila sea tan grande como sea posible, pero sin llegarlo a ser demasiado como para que tu app sea cancelada por el sistema operativo debido a que está agotando la memoria del sistema. Sin embargo, el sistema en tiempo de ejecución de Mono evita en lo posible que el tamaño de la pila se expanda. Puedes expandir la pila en forma manual, pre-asignando un espacio de reserva durante el arranque (es decir, puedes instanciar un objeto “inútil” que es asignado meramente para efectos del gestor de memoria):-

//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;
}

Una pila suficientemente grande no deberá quedar completamente llena al estar realizando una recolección durante las pausas en el ritmo del juego. Cuando una pausa ocurre, puedes solicitar una recolección de forma explícita:-

System.GC.Collect();

Una vez más, debes tener cuidado al usar esta estrategia, y pon atención a las estadísticas del profiles en lugar de sólo asumir que está teniendo el efecto deseado.

Pools de Objetos Reutilizables

Hay muchos casos donde puedes evitar la producción de basura con sólo reducir el número de objetos que son creados y destruidos. Hay ciertos tipos de objetos en los juegos, tales como proyectiles, que pueden ser encontrados una y otra vez aunque sólo un pequeño número estará siempre en el juego en ese instante. En casos como este, a menudo es posible reutilizar los objetos en vez de destruir los viejos y reemplazarlos con nuevos.

Más información

La gestión de memoria es un tema sutil y complejo al cual se le ha dedicado una gran cantidad de esfuerzo académico. Si estás interesado en aprender más sobre esto, la página memorymanagement.org es un excelente recurso que agrupa muchas publicaciones y artículos en línea. Más información sobre pooling de objetos puede ser encontrada en esta página de Wikipedia y también en Sourcemaking.com.

Execution Order of Event Functions (Orden de Ejecución de Funciones de Evento)
Compilación Dependiente a la Plataforma