Previsão de recursos: Compartilhamento genérico completo IL2CPP no Unity 2022.1 beta

O compartilhamento genérico completo permite que o senhor escreva códigos mais expressivos e mais fáceis de testar. Isso não apenas elimina toda uma classe de erros de script detectáveis no tempo de execução, mas também garante que o código em plataformas como dispositivos móveis e consoles se comporte de forma mais previsível. Continue lendo para saber como.
Os genéricos são recursos poderosos do C#. Eles permitem que o código expresse comportamentos independentemente dos tipos. Como desenvolvedores, esperamos que List<string> se comporte exatamente como List<int> ou List<T>, em que T é qualquer tipo.
Durante anos, o IL2CPP usou o compartilhamento genérico nos casos em que T é um tipo de referência (string, objeto, etc.) Isso funciona bem porque os tipos de referência no C# são sempre representados por um ponteiro, portanto, o tamanho e a implementação de List<string> corresponderão ao tamanho e à implementação de List<object>. Mas o que acontece se T for um int (quatro bytes) em um sistema de 64 bits (onde os ponteiros têm oito bytes)? O IL2CPP deve gerar código especial para List<int>, List<double>, List<MyValueType> e assim por diante.
É por isso que, no Unity 2022.1, o IL2CPP já gera um código especial que pode lidar com List<T> para qualquer tipo de T, referência ou valor. Essa tecnologia é chamada de Full Generic Sharing.
Embora os métodos virtuais genéricos sejam recursos expressivos do C# que funcionam bem com a compilação just-in-time (JIT), eles são difíceis de implementar em casos de compilação ahead-of-time (AOT), como o IL2CPP. É aí que entra o Compartilhamento genérico completo.
Vamos dar uma olhada em um exemplo de método virtual genérico do Manual da 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);
}Esse código demonstra a expressividade dos métodos virtuais genéricos. Em outras palavras, podemos enviar dados de qualquer tipo (a "mensagem") de qualquer classe que implemente a interface IManager para qualquer classe que implemente a interface IReceiver. Com o IL2CPP no Unity 2021.2, esse código aparentemente simples não funciona. Em tempo de execução, o seguinte erro aparece no registro do jogador:
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>:0Vamos analisar esse erro.
Como a chamada para Send Message no método Start ocorre por meio de uma interface (IManager, que é a parte "virtual" do virtual genérico), o IL2CPP não detecta qual método será chamado em tempo de execução quando o código for compilado.
O senhor pode estar se perguntando: Por que o IL2CPP não consegue entender isso? Bem, pode! É possível que o IL2CPP pesquise todo o código disponível em tempo de compilação e determine os locais onde essa chamada pode acabar. Mas essa busca é dispendiosa; ela toma um tempo precioso enquanto o senhor aguarda a compilação do projeto e pode fazer com que o IL2CPP gere código extra que nunca será chamado, aumentando o tamanho final do executável.
O argumento --generic-virtual-method-iterations (mencionado na mensagem de erro) permite que o senhor informe ao IL2CPP quanto tempo ele deve gastar na busca. Para um compilador JIT, esse tipo de chamada de método virtual genérico é muito simples. Ele pode "ver" o método de destino em tempo de execução e fazer a coisa certa. No Unity 2022.1, o IL2CPP aprendeu o mesmo truque. Agora, ele gera uma versão nova e especial do SendMessage, a versão totalmente compartilhada.
Isso funcionará independentemente do tipo T, de referência ou de valor, o que significa que, se o IL2CPP não puder ver qual deve ser o método de destino no momento da compilação, ele chamará essa versão totalmente compartilhada. O código C# é igualmente expressivo, funciona em tempo de execução e compila com a mesma rapidez.
A tecnologia Full Generic Sharing é incrivelmente útil, pois permite que o código em plataformas AOT se comporte muito mais como código em plataformas JIT. Isso resulta em menos surpresas no tempo de execução.
Acontece que esses erros de ExecutionEngineException também aparecem em outros casos. Sempre que o IL2CPP não consegue determinar qual código deve ser executado, esse erro pode ocorrer. Vemos isso com frequência em serializadores, em que alguns novos dados serializados são desserializados para um tipo que o IL2CPP não consegue entender. Mas no Unity 2022.1, o IL2CPP não produz mais um ExecutionEngineException, eliminando toda uma classe de erros que são difíceis de corrigir.
Considere também que alguns códigos usam tipos genéricos recursivos aninhados. Como o IL2CPP pode continuar processando esses tipos em tempo de compilação infinitamente, temos que impor um limite de tempo para o processo de compilação.
O IL2CPP costumava produzir o seguinte erro quando o seu código precisava de alguns desses tipos profundamente aninhados em tempo de execução: "O IL2CPP encontrou um tipo gerenciado que não pode ser convertido antecipadamente. O tipo usa tipos genéricos ou de matriz, que são aninhados além da profundidade máxima que pode ser convertida." Atualmente, o Full Generic Sharing permite que o IL2CPP use uma implementação que nunca falha, portanto, o senhor não encontrará mais essa mensagem de erro.
Imagine que o senhor tenha um projeto que deseja redimensionar e tornar o mais pequeno possível. Embora o senhor possa ter um código executável para List<int>, List<double> e List<string>, talvez também queira repensar o equilíbrio de tantas implementações diferentes.
Não seria ótimo ter apenas uma implementação genérica totalmente compartilhada para qualquer List<T>? Bem, verifique a opção IL2CPP Code Generation "Faster (smaller) builds" (Compilações mais rápidas (menores)) em Player Settings (Configurações do jogador). Ele aproveita o Full Generic Sharing para fornecer ao senhor o menor código executável possível com o tempo de compilação mais rápido possível, sem mencionar as compilações incrementais rápidas. Se o senhor decidir usar List<DateTime> (ou qualquer outro T) no projeto, o IL2CPP não precisará mais gerar ou compilar um novo código para essa implementação.
Se o senhor quiser começar a escrever códigos que aproveitem o IL2CPP Full Generic Sharing, basta baixar o Unity 2022.1 beta no Unity Hub ou em nossa página de download. Lembre-se de que a versão beta não se destina ao uso em projetos em fase de produção, portanto, não deixe de fazer backup dos projetos existentes.
Dito isso, gostaríamos de saber como o Unity 2022.1 funciona para o senhor. Visite o fórum da versão beta e deixe sua opinião. Gostaríamos muito de receber seu feedback sobre o Full Generic Sharing ou qualquer outro recurso com o qual o senhor esteja trabalhando atualmente. Como bônus por sua participação, cada relatório de bug original e reproduzível aumentará suas chances de ganhar um de nossos prêmios de sorteio. Veja os detalhes na publicação do blog sobre a versão beta.
