Vista previa del reportaje: Compartición genérica completa de IL2CPP en Unity 2022.1 beta

JOSH PETERSON / UNITY TECHNOLOGIESSenior Software Engineer
Dec 15, 2021|12 minutos
Vista previa del reportaje: Compartición genérica completa de IL2CPP en Unity 2022.1 beta
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.

La compartición genérica completa le permite escribir código más expresivo y fácil de probar. No sólo elimina toda una clase de errores de scripting detectables en tiempo de ejecución, sino que garantiza que el código en plataformas como dispositivos móviles y consolas se comporte de forma más predecible. Siga leyendo para saber cómo.

¿De qué se trata?

Los genéricos son potentes características de C#. Permiten que el código exprese comportamientos independientemente de los tipos. Como desarrolladores, esperamos que List<string> se comporte igual que List<int> o List<T>, donde T es cualquier tipo.

Durante años, IL2CPP ha utilizado la compartición genérica en los casos en los que T es un tipo de referencia (cadena, objeto, etc.) Esto funciona bien porque los tipos de referencia en C# siempre se representan mediante un puntero, por lo que el tamaño y la implementación de List<string> coincidirán con el tamaño y la implementación de List<object>. Pero, ¿qué ocurre si T es un int (cuatro bytes) en un sistema de 64 bits (donde los punteros son de ocho bytes)? IL2CPP debe generar código especial para List<int>, List<double>, List<MyValueType>, etc.

Es por eso que en Unity 2022.1, IL2CPP ya genera código especial que puede manejar List<T> para cualquier tipo T, referencia o valor. Esta tecnología se denomina Compartición Genérica Completa.

¿Qué problemas resuelve?

Mientras que los métodos virtuales genéricos son características expresivas de C# que funcionan bien con la compilación justo a tiempo (JIT), son difíciles de implementar para los casos de compilación por adelantado (AOT), como IL2CPP. Ahí es donde entra en juego el reparto genérico completo.

Veamos un ejemplo de método virtual genérico del manual de Unity:

using UnityEngine;
using System;

public class AOTProblemExample : MonoBehaviour, IReceiver
{
    public enum AnyEnum 
    {
        Zero,
        One,
    }

    void Start() 
    {
        // Subtle trigger: The type of manager *must* be
        // IManager, not Manager, to trigger the AOT problem.
        IManager manager = new Manager();
        manager.SendMessage(this, AnyEnum.Zero);
    }

    public void OnMessage<T>(T value) 
    {
        Debug.LogFormat("Message value: {0}", value);
    }
}

public class Manager : IManager 
{
    public void SendMessage<T>(IReceiver target, T value) {
        target.OnMessage(value);
    }
}

public interface IReceiver
{
    void OnMessage<T>(T value);
}

public interface IManager 
{
    void SendMessage<T>(IReceiver target, T value);
}

Este código demuestra la expresividad de los métodos virtuales genéricos. En otras palabras, podemos enviar datos de cualquier tipo (el "mensaje") desde cualquier clase que implemente la interfaz IManager a cualquier clase que implemente la interfaz IReceiver. Con IL2CPP en Unity 2021.2, este código aparentemente sencillo no funciona. En tiempo de ejecución, aparece el siguiente error en el registro del jugador:

ExecutionEngineException: Attempting to call method 'Test::OnMessage<Test+AnyEnum>' for which no ahead of time (AOT) code was generated. Consider increasing the --generic-virtual-method-iterations=1 argument

at Manager.SendMessage[T] (IReceiver target, T value) [0x00000] in <00000000000000000000000000000000>:0

at Test.Start () [0x00000] in <00000000000000000000000000000000>:0

Desgranemos este error.

Dado que la llamada a Enviar mensaje en el método Inicio se produce a través de una interfaz (IManager, que es la parte "virtual" de virtual genérico), IL2CPP no detecta qué método se llamará en tiempo de ejecución cuando se compila el código.

Se estará preguntando: ¿Por qué no se da cuenta el IL2CPP? ¡Pues sí que puede! Es posible que IL2CPP busque en todo el código disponible en tiempo de compilación y determine los lugares en los que podría acabar esta llamada. Pero esta búsqueda es cara; lleva un tiempo precioso mientras espera a que se construya el proyecto, y puede hacer que IL2CPP genere código extra que nunca será llamado, aumentando el tamaño final del ejecutable.

El argumento --generic-virtual-method-iterations (mencionado en el mensaje de error) le permite indicar a IL2CPP cuánto tiempo debe dedicar a la búsqueda. Para un compilador JIT, este tipo de llamada a métodos virtuales genéricos es realmente sencillo. Puede "ver" el método de destino en tiempo de ejecución y hacer lo correcto. En Unity 2022.1, IL2CPP ha aprendido el mismo truco. Ahora genera una nueva versión especial de SendMessage: la versión totalmente compartida.

Esto funcionará independientemente del tipo T, referencia o valor, lo que significa que si IL2CPP no puede ver cuál debe ser el método de destino en tiempo de compilación, llamará a esta versión totalmente compartida en su lugar. El código C# es igual de expresivo, funciona en tiempo de ejecución y se compila igual de rápido.

Dígame más

La tecnología Full Generic Sharing es increíblemente útil porque permite que el código de las plataformas AOT se comporte de forma mucho más parecida al código de las plataformas JIT. Esto conduce a menos sorpresas en tiempo de ejecución.

Resulta que estos errores de ExecutionEngineException también aparecen en otros casos. Cuando IL2CPP no consigue determinar qué código ejecutar, puede producirse este error. A menudo vemos esto en serializadores, donde algunos nuevos datos serializados deserializan a un tipo que IL2CPP no puede adivinar. Pero en Unity 2022.1, IL2CPP ya no produce una ExecutionEngineException, eliminando toda una clase de errores difíciles de rectificar.

Considere también que parte del código utiliza tipos genéricos recursivos anidados. Dado que IL2CPP puede seguir procesando estos tipos en tiempo de compilación infinitamente, tenemos que imponer un límite al tiempo que debe durar el proceso de compilación.

IL2CPP solía producir el siguiente error cuando su código necesitaba alguno de esos tipos profundamente anidados en tiempo de ejecución: "IL2CPP ha encontrado un tipo gestionado que no puede convertir de antemano. El tipo utiliza tipos genéricos o de matriz, que están anidados más allá de la profundidad máxima que se puede convertir". En la actualidad, la Compartición Genérica Completa permite a IL2CPP utilizar una implementación que nunca falla, por lo que ya no se encontrará con este mensaje de error.

Imagine que tiene un proyecto que quiere redimensionar y hacer lo más pequeño posible. Si bien es posible que disponga de código ejecutable para List<int>, List<double> y List<string>, también es posible que desee replantearse el equilibrio de tantas implementaciones diferentes.

¿No sería estupendo disponer de una única implementación genérica totalmente compartida para cualquier List<T>? Bien, compruebe la opción de generación de código IL2CPP "Construcciones más rápidas (más pequeñas)" en los ajustes del reproductor. Aprovecha la compartición genérica completa para ofrecerle el código ejecutable más pequeño posible con el tiempo de compilación más rápido posible, por no hablar de las rápidas compilaciones incrementales. Si decide utilizar List<DateTime> (o cualquier otra T) en el proyecto, IL2CPP ya no necesitará generar o compilar código nuevo para esa implementación.

Iniciarse en la beta

Si desea empezar a escribir código que aproveche el uso compartido genérico completo de IL2CPP, sólo tiene que descargar Unity 2022.1 beta desde Unity Hub o en nuestra página de descargas. Recuerde que la beta no está pensada para su uso en proyectos en fase de producción, así que asegúrese de realizar copias de seguridad de sus proyectos existentes.

Dicho esto, nos encantaría saber cómo le funciona Unity 2022.1. Visite el foro de la versión beta para dejarnos su opinión. Apreciaríamos enormemente sus comentarios sobre el uso compartido genérico completo o cualquier otra función con la que esté trabajando actualmente. Como bonificación por su participación, cada informe de error original y reproducible aumentará sus posibilidades de ganar uno de nuestros premios del sorteo. Encuentre los detalles en la entrada del blog de la versión beta.