Feature-Vorschau: IL2CPP Vollständige generische Freigabe in Unity 2022.1 beta

Mit der vollständigen generischen Freigabe können Sie Code schreiben, der ausdrucksstärker und einfacher zu testen ist. Es beseitigt nicht nur eine ganze Klasse von Skriptfehlern, die zur Laufzeit erkannt werden können, sondern sorgt auch dafür, dass sich Code auf Plattformen wie mobilen Geräten und Konsolen vorhersehbarer verhält. Lesen Sie weiter, um zu erfahren, wie.
Generics sind leistungsstarke Funktionen von C#. Sie ermöglichen es dem Code, Verhaltensweisen unabhängig von Typen auszudrücken. Als Entwickler erwarten wir, dass sich List<string> genauso verhält wie List<int> oder List<T>, wobei T ein beliebiger Typ ist.
Seit Jahren verwendet IL2CPP das generische Sharing in Fällen, in denen T ein Referenztyp ist (String, Objekt usw.) Das funktioniert gut, weil Referenztypen in C# immer durch einen Zeiger dargestellt werden, so dass die Größe und Implementierung von List<string> der Größe und Implementierung von List<object> entspricht. Aber was passiert, wenn T ein int (vier Bytes) auf einem 64-Bit-System ist (wo Zeiger acht Bytes lang sind)? IL2CPP muss speziellen Code für List<int>, List<double>, List<MyValueType> und so weiter erzeugen.
Deshalb generiert IL2CPP in Unity 2022.1 bereits speziellen Code, der List<T> für jeden T-, Referenz- oder Wertetyp verarbeiten kann. Diese Technologie wird Full Generic Sharing genannt.
Während generische virtuelle Methoden ausdrucksstarke Funktionen von C# sind, die gut mit der Just-in-Time (JIT) Kompilierung funktionieren, sind sie für die AOT-Kompilierung (AOT = ahead of time), wie IL2CPP, schwer zu implementieren. Hier kommt das Full Generic Sharing ins Spiel.
Werfen wir einen Blick auf ein Beispiel für eine generische virtuelle Methode aus dem Unity-Handbuch:
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);
}Dieser Code veranschaulicht die Ausdruckskraft von generischen virtuellen Methoden. Mit anderen Worten, wir können Daten eines beliebigen Typs (die "Nachricht") von jeder Klasse, die die IManager-Schnittstelle implementiert, an jede Klasse senden, die die IReceiver-Schnittstelle implementiert. Mit IL2CPP in Unity 2021.2 funktioniert dieser scheinbar einfache Code nicht. Zur Laufzeit erscheint der folgende Fehler im Player-Protokoll:
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>:0Lassen Sie uns diesen Fehler entschlüsseln.
Da der Aufruf von Send Message in der Start-Methode über eine Schnittstelle (IManager, d.h. der "virtuelle" Teil von generic virtual) erfolgt, erkennt IL2CPP nicht, welche Methode zur Laufzeit aufgerufen wird, wenn der Code kompiliert wird.
Das werden Sie sich vielleicht fragen: Warum kann IL2CPP das nicht herausfinden? Nun, das kann es! IL2CPP ist in der Lage, den gesamten zur Kompilierzeit verfügbaren Code zu durchsuchen und die Stellen zu ermitteln, an denen dieser Aufruf landen könnte. Diese Suche ist jedoch kostspielig. Sie kostet wertvolle Zeit, während Sie darauf warten, dass das Projekt erstellt wird, und sie kann dazu führen, dass IL2CPP zusätzlichen Code generiert, der nie aufgerufen wird, wodurch die endgültige ausführbare Datei größer wird.
Mit dem Argument --generic-virtual-method-iterations (in der Fehlermeldung erwähnt) können Sie IL2CPP mitteilen, wie viel Zeit es für die Suche aufwenden soll. Für einen JIT-Compiler ist diese Art von generischem virtuellem Methodenaufruf sehr einfach. Es kann die Zielmethode zur Laufzeit "sehen" und das Richtige tun. In Unity 2022.1 hat IL2CPP den gleichen Trick gelernt. Es erzeugt jetzt eine neue, spezielle Version von SendMessage - die vollständig freigegebene Version.
Dies funktioniert unabhängig vom T-, Referenz- oder Wertetyp, d.h. wenn IL2CPP zur Kompilierzeit nicht erkennen kann, was die Zielmethode sein soll, ruft es stattdessen diese vollständig freigegebene Version auf. Der C#-Code ist genauso ausdrucksstark, funktioniert zur Laufzeit und lässt sich genauso schnell kompilieren.
Die Full Generic Sharing-Technologie ist unglaublich nützlich, da sie es ermöglicht, dass sich Code auf AOT-Plattformen viel mehr wie Code auf JIT-Plattformen verhält. Dies führt zu weniger Überraschungen zur Laufzeit.
Es hat sich herausgestellt, dass diese ExecutionEngineException-Fehler auch in anderen Fällen auftauchen. Immer wenn IL2CPP nicht feststellen kann, welcher Code ausgeführt werden soll, kann dieser Fehler auftreten. Wir sehen das oft bei Serialisierern, wo einige neue serialisierte Daten zu einem Typ deserialisiert werden, den IL2CPP nicht erahnen kann. Aber in Unity 2022.1 erzeugt IL2CPP keine ExecutionEngineException mehr, wodurch eine ganze Klasse von Fehlern vermieden wird, die schwer zu beheben sind.
Bedenken Sie auch, dass einige Codes verschachtelte rekursive generische Typen verwenden. Da IL2CPP die Verarbeitung dieser Typen zur Kompilierzeit unendlich fortsetzen kann, müssen wir eine Begrenzung der Zeit festlegen, die der Erstellungsprozess in Anspruch nehmen darf.
IL2CPP produzierte früher die folgende Fehlermeldung, wenn Ihr Code einige dieser tief verschachtelten Typen zur Laufzeit benötigte: "IL2CPP ist auf einen verwalteten Typ gestoßen, den es nicht im Voraus konvertieren kann. Der Typ verwendet generische oder Array-Typen, die über die maximale Tiefe, die konvertiert werden kann, hinaus verschachtelt sind." Mit Full Generic Sharing kann IL2CPP heute eine Implementierung verwenden, die niemals fehlschlägt, so dass diese Fehlermeldung nicht mehr auftritt.
Stellen Sie sich vor, Sie haben ein Projekt, das Sie verkleinern und so klein wie möglich machen möchten. Während Sie vielleicht ausführbaren Code für List<int>, List<double> und List<string> haben, sollten Sie vielleicht auch die Balance zwischen so vielen verschiedenen Implementierungen überdenken.
Wäre es nicht großartig, nur eine einzige, gemeinsame generische Implementierung für jede List<T> zu haben? Schauen Sie sich die IL2CPP Codegenerierungsoption "Schnellere (kleinere) Builds" in den Player-Einstellungen an. Es nutzt die volle generische Freigabe, um Ihnen den kleinstmöglichen ausführbaren Code mit der schnellstmöglichen Erstellungszeit zu liefern - ganz zu schweigen von den schnellen inkrementellen Builds. Wenn Sie sich entscheiden, List<DateTime> (oder ein anderes T) im Projekt zu verwenden, muss IL2CPP keinen neuen Code für diese Implementierung mehr generieren oder kompilieren.
Wenn Sie anfangen möchten, Code zu schreiben, der IL2CPP Full Generic Sharing nutzt, laden Sie einfach Unity 2022.1 beta vom Unity Hub oder von unserer Download-Seite herunter . Denken Sie daran, dass die Beta-Version nicht für die Verwendung in Produktionsprojekten gedacht ist. Stellen Sie also sicher, dass Sie Ihre bestehenden Projekte sichern.
Trotzdem würden wir gerne wissen, wie Unity 2022.1 bei Ihnen funktioniert. Bitte besuchen Sie das Beta-Forum, um uns Ihre Meinung mitzuteilen. Wir würden uns sehr über Ihr Feedback zu Full Generic Sharing oder einer anderen Funktion freuen, mit der Sie gerade arbeiten. Als Bonus für Ihre Beteiligung erhöht jeder originelle und reproduzierbare Fehlerbericht Ihre Chancen auf einen unserer Gewinnspielpreise. Die Details finden Sie im Blogbeitrag zur Beta-Version.
