Componentes internos de IL2CPP: Integración del recolector de basura

JOSH PETERSON / UNITY TECHNOLOGIESSenior Software Engineer
Jul 9, 2015|8 minutos
Componentes internos de IL2CPP: Integración del recolector de basura
Para tu comodidad, tradujimos esta página mediante traducción automática. No podemos garantizar la precisión ni la confiabilidad del contenido traducido. Si tienes alguna duda sobre la precisión del contenido traducido, consulta la versión oficial en inglés de la página web.
Esta es la séptima publicación de la serie IL2CPP Internals. En esta publicación, exploraremos un poco sobre cómo el entorno de ejecución IL2CPP se integra con un recolector de basura. En concreto, veremos cómo se comunican las raíces GC en el código administrado al recolector de basura nativo.

Al igual que todas las publicaciones de esta serie, esta publicación trata sobre detalles de implementación que pueden cambiar y probablemente cambiarán en el futuro. En esta publicación, analizaremos específicamente algunas API internas utilizadas por el código de ejecución para comunicarse con el recolector de basura. Estas API no cuentan con soporte público y no debe intentar llamarlas desde ningún código en un proyecto real. Pero este es un post sobre aspectos internos, así que profundicemos en ello.

Recolección de basura

No analizaré técnicas generales de recolección de basura en esta publicación, ya que es un tema amplio y variado, con mucha investigación existente e información publicada. Para continuar, piense en un GC como un algoritmo que desarrolla un gráfico dirigido de referencias de objetos. Si un objeto Child es utilizado por un objeto Parent (a través de un puntero en código nativo), entonces el gráfico se ve así:

image03

A medida que el GC explora la memoria en busca de un proceso, busca objetos que no tengan padre. Si encuentra uno, puede reutilizar la memoria de ese objeto en otra cosa.

Por supuesto, la mayoría de los objetos tendrán un padre de algún tipo, por lo que el GC realmente necesita saber qué objetos son los padres importantes. Me gusta pensar en ellos como los objetos que realmente utiliza su programa. En la terminología de GC , estas se denominan “raíces”. A continuación se muestra un ejemplo de un padre sin raíz.

image02

En este caso, el Padre 2 no tiene una raíz, por lo que el GC puede reutilizar la memoria del Padre 2 y el Hijo 2. Sin embargo, Padre 1 e Hijo 1 tienen una raíz, por lo que el GC no puede reutilizar su memoria. El programa todavía los está usando para algo.

Para .NET, hay tres tipos de raíces:

- Variables locales en la pila de cualquier hilo que ejecute código administrado

- Variables estáticas

- Objetos GCHandle

Veremos cómo IL2CPP se comunica con el recolector de basura sobre estos tres tipos de raíces.

La configuración

Para esta publicación, estoy usando Unity 5.1.0p1 en OSX y estoy construyendo para la plataforma iOS. Esto nos permitirá usar Xcode para ver cómo IL2CPP interactúa con el recolector de basura. Al igual que en las otras publicaciones de esta serie, utilizaré un proyecto de ejemplo con un solo script:

using System;
using System.Runtime.InteropServices;
using System.Threading;
using UnityEngine;

public class AnyClass {}

public class HelloWorld : MonoBehaviour {
private static AnyClass staticAnyClass = new AnyClass();
void Start () {
var thread = new Thread(AnotherThread);
thread.Start();
thread.Join();
var anyClassForGCHandle = new AnyClass();
var gcHandle = GCHandle.Alloc(anyClassForGCHandle);
}

private static void AnotherThread() {
var anyClassLocal = new AnyClass();
}
}

He habilitado la “Compilación de desarrollo” en el cuadro de diálogo Configuración de compilación y configuré la opción “Ejecutar en Xcode como” en un valor de “Depurar”. En el proyecto Xcode generado, primero busque la cadena “Start_m”. Debería ver el código generado para el método Start en la clase HelloWorld llamada HelloWorld_Start_m3.

Agregar variables locales de subproceso como raíces

Agregue un punto de interrupción en la función HelloWorld_Start_m3 en la línea donde se llama a Thread_Start_m9. Este método creará un nuevo hilo administrado, por lo que esperamos que ese hilo se agregue al GC como raíz. Podemos ver dónde sucede esto explorando los archivos de encabezado libil2cpp que vienen con Unity. En la instalación de Unity , abra el archivo Contents/Frameworks/il2cpp/libil2cpp/gc/gc-internal.h. Este archivo tiene varios métodos con el prefijo il2cpp_gc_ y sirve como parte de la API entre el entorno de ejecución de libil2cpp y el recolector de elementos no utilizados. Tenga en cuenta que esta no es una API pública, por lo tanto, no llame a estos métodos desde ningún código de proyecto real. Están sujetos a cambios o eliminación sin previo aviso.

Creemos un punto de interrupción en Xcode en la función il2cpp_gc_register_thread, usando Depurar > Puntos de interrupción > Crear punto de interrupción simbólico.

image04

Si luego ejecuta el proyecto en Xcode, notará que el punto de interrupción se alcanza casi inmediatamente. No podemos ver el código fuente aquí, ya que está construido en la biblioteca estática de tiempo de ejecución libil2cpp, pero podemos ver en la pila de llamadas que este hilo se crea en el método InitializeScriptingBackend, que se ejecuta cuando se inicia el reproductor.

image01

De hecho, veremos que este punto de interrupción se alcanza varias veces a medida que el reproductor crea cada hilo administrado que se usa internamente. Por ahora, puedes deshabilitar este punto de interrupción en Xcode y permitir que el proyecto continúe. Deberíamos alcanzar el punto de interrupción que establecimos anteriormente en el método HelloWorld_Start_m3.

Ahora estamos a punto de iniciar el hilo administrado creado por nuestro código de script, así que habilite el punto de interrupción en il2cpp_gc_register_thread nuevamente. Cuando llegamos a ese punto de interrupción, el primer hilo está esperando unirse a nuestro hilo creado, pero la pila de llamadas para el hilo creado muestra que apenas lo estamos iniciando:

image05

Cuando un hilo se registra en el recolector de basura, el GC trata todos los objetos en la pila local de ese hilo como raíces. Veamos el código generado para el método que ejecutamos en ese hilo (HelloWorld_AnotherThread_m4):

AnyClass_t1 * L_0 = (AnyClass_t1 *)il2cpp_codegen_object_new (AnyClass_t1_il2cpp_TypeInfo_var);
AnyClass__ctor_m0(L_0, /*hidden argument*/NULL);
V_0 = L_0;

Podemos ver una variable local, L_0, que el GC debe tratar como una raíz. Durante la (corta) vida útil de este hilo, el recolector de basura no puede reutilizar esta instancia del objeto AnyClass ni ningún otro objeto al que haga referencia. Las variables definidas en la pila son el tipo más común de raíces GC , ya que la mayoría de los objetos de un programa comienzan desde una variable local en un método que se ejecuta en un hilo administrado.

Cuando un hilo sale, se llama a la función il2cpp_gc_unregister_thread para indicarle al GC que deje de tratar los objetos en la pila de hilos como raíces. El GC puede entonces trabajar en la reutilización de la memoria para el objeto AnyClass representado en código nativo por L_0.

Variables estáticas

Sin embargo, algunas variables no viven en las pilas de llamadas de subprocesos. Estas son variables estáticas y el recolector de basura también debe manejarlas como raíces.

Cuando IL2CPP establece la representación nativa de una clase, agrupa todos los campos estáticos en una estructura C++ diferente de los campos de instancia de la clase. En Xcode, podemos saltar a la definición de la clase HelloWorld_t2:

struct  HelloWorld_t2  : public MonoBehaviour_t3
{
};

struct HelloWorld_t2_StaticFields{
// AnyClass HelloWorld::staticAnyClass
AnyClass_t1 * ___staticAnyClass_2;
};

Tenga en cuenta que IL2CPP no utiliza la palabra clave static de C++, ya que necesita controlar el diseño y la asignación de los campos estáticos para comunicarse correctamente con el GC. Cuando un tipo se utiliza por primera vez en tiempo de ejecución, el código libil2cpp inicializará el tipo. Parte de esta inicialización implica la asignación de memoria para la estructura HelloWorld_t2_StaticFields. Esta memoria se asigna con una llamada especial al GC: il2cpp_gc_alloc_fixed (también en el archivo gc-internal.h).

Esta llamada informa al recolector de basura que debe tratar la memoria asignada como una raíz, y el GC hace esto diligentemente durante la vida útil del proceso. Es posible establecer un punto de interrupción en la función il2cpp_gc_alloc_fixed en Xcode, pero se llama con bastante frecuencia (incluso para este proyecto simple), por lo que el punto de interrupción no es demasiado útil.

Objetos GCHandle

Supongamos que no desea utilizar una variable estática, pero aún así desea tener un poco más de control sobre cuándo se le permite al recolector de basura reutilizar la memoria para un objeto. Esto suele ser útil cuando necesitas pasar un puntero a un objeto administrado desde un código administrado a un código nativo. Si el código nativo tomará posesión de ese objeto, debemos decirle al recolector de basura que el código nativo ahora es una raíz en su gráfico de objetos. Esto funciona mediante el uso de un objeto administrado especial llamado GCHandle.

La creación de un GCHandle informa al código de tiempo de ejecución que un objeto administrado determinado debe tratarse como una raíz en el GC para que éste y cualquier objeto al que haga referencia no sean reutilizados. En IL2CPP, podemos ver la API de bajo nivel para lograr esto en el archivo Contents/Frameworks/il2cpp/libil2cpp/gc/GCHandle.h. Nuevamente, esta no es una API pública, pero es divertido investigarla. Coloquemos un punto de interrupción en la función GCHandle::New. Si dejamos que el proyecto continúe, deberíamos ver esta pila de llamadas:

image00

Tenga en cuenta que el código generado para nuestro método Start llama a GCHandle_Alloc_m11, que eventualmente crea un GCHandle e informa al recolector de basura que tenemos un nuevo objeto raíz.

Conclusión

Hemos analizado algunos métodos API internos para ver cómo el entorno de ejecución IL2CPP interactúa con el recolector de elementos no utilizados, permitiéndole saber qué objetos son las raíces que debe preservar. Tenga en cuenta que no hemos hablado en absoluto sobre qué recolector de basura utiliza IL2CPP. Actualmente se utiliza el GC Boehm-Demers-Weiser , pero hemos trabajado duro para aislar el recolector de basura detrás de una interfaz limpia. Actualmente tenemos planes para investigar la integración del recolector de basura de código abierto CoreCLR . Todavía no tenemos una fecha de envío firme para esta integración, pero esté atento a nuestra hoja de ruta pública para obtener actualizaciones.

Como de costumbre, apenas hemos arañado la superficie de la integración de GC en IL2CPP. Te animo a explorar más sobre cómo interactúan IL2CPP y el GC . Por favor, comparte tus ideas también.

La próxima vez, finalizaremos la serie sobre los aspectos internos de IL2CPP analizando cómo probamos el código IL2CPP.