10000 Update()-Aufrufe

Unbekannter Blocktyp "codeBlock", bitte geben Sie einen Serializer dafür in der `serializers.types` prop an
Für einen erfahrenen Entwickler ist dieser Code ein wenig seltsam.
1. Es ist nicht klar, wie genau diese Methode aufgerufen wird.
2. Es ist nicht klar, in welcher Reihenfolge diese Methoden aufgerufen werden, wenn Sie mehrere Objekte in einer Szene haben.
3. Dieser Codestil funktioniert nicht mit Intellisense.
Nein, Unity verwendet nicht System.Reflection, um jedes Mal eine magische Methode zu finden, wenn es eine aufrufen muss.
Stattdessen wird beim ersten Zugriff auf ein MonoBehaviour eines bestimmten Typs das zugrundeliegende Skript durch die Skripting-Laufzeit (entweder Mono oder IL2CPP) daraufhin überprüft, ob es irgendwelche magischen Methoden definiert hat, und diese Information wird zwischengespeichert. Wenn ein MonoBehaviour eine bestimmte Methode hat, wird es zu einer entsprechenden Liste hinzugefügt, z. B. wenn ein Skript eine Update-Methode definiert hat, wird es zu einer Liste von Skripten hinzugefügt, die bei jedem Frame aktualisiert werden müssen.
Während des Spiels iteriert Unity einfach durch diese Listen und führt Methoden daraus aus - so einfach ist das. Aus diesem Grund spielt es auch keine Rolle, ob Ihre Aktualisierungsmethode öffentlich oder privat ist.
Die Reihenfolge wird durch die Einstellungen der Skriptausführungsreihenfolge festgelegt (Menü: Bearbeiten > Projekteinstellungen > Skriptausführungsreihenfolge). Es ist vielleicht nicht der beste Weg, die Reihenfolge von 1000 Skripten manuell festzulegen, aber wenn Sie wollen, dass ein Skript nach allen anderen ausgeführt wird, ist dieser Weg akzeptabel. Natürlich wollen wir in Zukunft eine bequemere Möglichkeit haben, die Ausführungsreihenfolge festzulegen, zum Beispiel mit einem Attribut im Code.
Wir alle verwenden eine IDE, um unsere C#-Skripte in Unity zu bearbeiten. Die meisten von ihnen mögen keine magischen Methoden, bei denen sie nicht herausfinden können, wo sie aufgerufen werden, wenn überhaupt. Dies führt zu Warnungen und erschwert die Navigation im Code.
Manchmal fügen Entwickler eine abstrakte Klasse hinzu, die MonoBehaviour erweitert, nennen sie BaseMonoBehaviour oder ähnlich und lassen jedes Skript in ihrem Projekt diese Klasse erweitern. Sie haben einige grundlegende nützliche Funktionen zusammen mit einem Haufen virtueller magischer Methoden wie dieser eingebaut:
Unbekannter Blocktyp "codeBlock", bitte geben Sie einen Serializer dafür in der `serializers.types` prop an
Diese Struktur macht die Verwendung von MonoBehaviours in Ihrem Code logischer, hat aber einen kleinen Makel. Ich wette, du hast es schon herausgefunden...
Alle Ihre MonoBehaviours werden in allen Update-Listen sein, die Unity intern verwendet. Alle diese Methoden werden bei jedem Frame für alle Ihre Skripte aufgerufen und machen meistens gar nichts!
Man könnte fragen, warum sich jemand für eine leere Methode interessieren sollte? Die Sache ist, dass diese die Anrufe von nativen C++ Land zu verwalteten C # Land sind, haben sie einen Preis. Mal sehen, wie hoch diese Kosten sind.
Für diesen Beitrag habe ich ein kleines Beispielprojekt erstellt, das auf Github verfügbar ist. Es hat 2 Szenen, die durch Tippen auf ein Gerät oder Drücken einer beliebigen Taste im Editor geändert werden können:
(1) In der ersten Szene werden 10000 MonoBehaviours mit diesem Code darin erstellt:
Unbekannter Blocktyp "codeBlock", bitte geben Sie einen Serializer dafür in der `serializers.types` prop an
(2) In der zweiten Szene werden weitere 10000 MonoBehaviours erstellt, aber anstelle eines Updates haben sie eine benutzerdefinierte UpdateMe-Methode, die von einem Managerskript bei jedem Frame wie folgt aufgerufen wird:
Unbekannter Blocktyp "codeBlock", bitte geben Sie einen Serializer dafür in der `serializers.types` prop an
Das Testprojekt wurde auf 2 iOS-Geräten ausgeführt, die mit Mono und IL2CPP im Nicht-Entwicklungsmodus in der Release-Konfiguration kompiliert wurden. Die Zeit wurde wie folgt gemessen:
Richten Sie eine Stoppuhr in der ersten aufgerufenen Aktualisierung ein (konfiguriert in Skriptausführungsreihenfolge),
Stoppen Sie die Stoppuhr bei LateUpdate,
Bilden Sie einen Durchschnitt über einige Minuten.
Unity-Version: 5.2.2f1
iOS-Version: 9.0

WOW! Das ist eine ganze Menge! Da muss etwas mit dem Test nicht stimmen!
Eigentlich habe ich nur vergessen, die Skriptaufrufoptimierung auf Schnell, aber keine Ausnahmen einzustellen, aber jetzt können wir sehen, wie sich diese spezielle Einstellung auf die Leistung auswirkt... nicht, dass das bei IL2CPP noch jemanden interessiert.

OK, so ist es besser. Wechseln wir zu IL2CPP.

Hier sehen wir zwei Dinge:
1. Diese besondere Optimierung ist in IL2CPP immer noch sinnvoll.
2. IL2CPP ist noch verbesserungsfähig, und während ich diesen Beitrag schreibe, arbeiten die Teams von Scripting und IL2CPP intensiv an der Verbesserung der Leistung. Der neueste Scripting-Zweig enthält zum Beispiel Optimierungen, die den Testlauf um 35 % beschleunigen.
Ich werde gleich erklären, was Unity unter der Haube macht. Aber jetzt wollen wir den Code unseres Managers ändern, um ihn 5 Mal schneller zu machen!
Wenn Sie diese großartige Serie von Beiträgen über IL2CPP-Interna noch nicht gelesen haben, sollten Sie das sofort tun, nachdem Sie diesen Beitrag gelesen haben!
Es stellt sich heraus, dass man, wenn man eine Liste mit 10000 Elementen in jedem Frame durchlaufen wollte, besser ein Array statt einer Liste verwenden sollte, weil der in diesem Fall erzeugte C++-Code einfacher ist und der Zugriff auf ein Array einfach schneller ist.
Im nächsten Test habe ich List<ManagedUpdateBehavior> in ManagedUpdateBehavior[] geändert.

Das sieht viel besser aus!
Aktualisierung: Ich habe den Test mit Array auf Mono durchgeführt und 0,23ms erhalten.
Wir haben herausgefunden, dass der Aufruf von Funktionen von C++ nach C# nicht schnell ist, aber lassen Sie uns herausfinden, was Unity tatsächlich tut, wenn es Updates für all diese Objekte aufruft. Am einfachsten geht das mit dem Time Profiler von Apple Instruments.
Beachten Sie, dass es hier nicht um Mono gegen Mono geht. IL2CPP-Test - das meiste, was weiter unten beschrieben wird, gilt auch für ein Mono iOS-Build.
Ich habe den Test auf dem iPhone 6 mit Time Profiler gestartet, ein paar Minuten Daten aufgezeichnet und ein einminütiges Intervall zur Überprüfung ausgewählt. Wir sind an allem interessiert, was von dieser Linie ausgeht:
Unbekannter Blocktyp "codeBlock", bitte geben Sie einen Serializer dafür in der `serializers.types` prop an
Wenn Sie die Instrumente noch nicht verwendet haben, sehen Sie auf der rechten Seite Funktionen, die nach Ausführungszeit sortiert sind, sowie andere Funktionen, die sie aufrufen. Die Spalte ganz links gibt die CPU-Zeit in ms und % für diese Funktionen und die von ihnen aufgerufenen Funktionen zusammen an, die zweite Spalte links ist die Selbstausführungszeit der Funktion. Da die CPU bei diesem Experiment nicht vollständig von Unity ausgelastet wurde, werden in einem 60-Sekunden-Intervall 10 Sekunden CPU-Zeit für unsere Updates verbraucht. Offensichtlich sind wir an Funktionen interessiert, die die meiste Zeit zur Ausführung benötigen.
Ich habe meine verrückten Photoshop-Fähigkeiten genutzt und einige Bereiche farblich gekennzeichnet, damit ihr besser versteht, was hier vor sich geht.

In der Mitte sehen Sie unsere Update-Methode oder wie IL2CPP sie nennt - UpdateBehavior_Update_m18. Aber bevor es so weit ist, macht die Einheit noch eine Menge anderer Dinge.
Unity geht alle Behaviours durch, um sie zu aktualisieren. Eine spezielle Iterator-Klasse, SafeIterator, sorgt dafür, dass nichts kaputt geht, wenn jemand beschließt, den nächsten Eintrag in der Liste zu löschen. Allein die Iteration über alle registrierten Behaviours dauert 1517ms von insgesamt 9979ms.
Als nächstes führt Unity eine Reihe von Überprüfungen durch, um sicherzustellen, dass es eine gültige Methode für ein aktives GameObject aufruft, das initialisiert und dessen Start-Methode aufgerufen wurde. Sie wollen doch nicht, dass Ihr Spiel abstürzt, wenn Sie ein GameObject während des Updates zerstören, oder? Diese Prüfungen nehmen weitere 2188ms von insgesamt 9979ms in Anspruch.
Unity erstellt eine Instanz von ScriptingInvocationNoArgs (die einen Aufruf von der nativen Seite zur verwalteten Seite darstellt) zusammen mit ScriptingArguments und beauftragt die virtuelle Maschine IL2CPP mit dem Aufruf der Methode (Funktion scripting_method_invoke). Dieser Schritt dauert 2061ms von insgesamt 9979ms.
Die Funktion scripting_method_invoke prüft, ob die übergebenen Argumente gültig sind (900ms) und ruft dann die Methode Runtime::Invoke der virtuellen Maschine IL2CPP auf (1520ms). Zunächst prüft Runtime::Invoke, ob eine solche Methode existiert (1018ms). Anschließend wird eine generierte RuntimeInvoker-Funktion für die Methodensignatur aufgerufen (283ms). Sie ruft wiederum unsere Update-Funktion auf, die laut Time Profiler 42 ms für die Ausführung benötigt.
Und ein schöner bunter Tisch.

Lassen Sie uns nun Time Profiler mit dem Managertest verwenden. Sie können auf dem Screenshot sehen, dass es die gleichen Methoden (einige von ihnen nehmen weniger als 1ms insgesamt, so dass sie nicht einmal angezeigt werden), aber die meiste Ausführungszeit ist tatsächlich gehen, um UpdateMe-Funktion (oder wie IL2CPP ruft es - ManagedUpdateBehavior_UpdateMe_m14). Außerdem gibt es eine Null-Prüfung, die von IL2CPP eingefügt wird, um sicherzustellen, dass das Array, über das wir iterieren, nicht Null ist.
Das nächste Bild verwendet die gleichen Farben.

Was meinen Sie nun, sollte man sich um einen kleinen Methodenaufruf kümmern?
Um ehrlich zu sein, ist dieser Test nicht ganz fair. Unity leistet großartige Arbeit, um Sie und Ihr Spiel vor unbeabsichtigtem Verhalten und Abstürzen zu bewahren: Ist dieses GameObject aktiv? Wurde es nicht während dieser Aktualisierungsschleife zerstört? Existiert eine Update-Methode für das Objekt? Was ist mit einem MonoBehaviour zu tun, das in dieser Aktualisierungsschleife erstellt wurde? - Mein Managerskript behandelt nichts dergleichen, es durchläuft lediglich eine Liste der zu aktualisierenden Objekte.
In der realen Welt wäre das Manager-Skript wahrscheinlich komplizierter und langsamer in der Ausführung gewesen. Aber in diesem Fall bin ich der Entwickler - ich weiß, was mein Code tun soll, und ich entwerfe meine Managerklasse mit dem Wissen, welches Verhalten in meinem Spiel möglich ist und welches nicht. Unity verfügt leider nicht über dieses Wissen.
Natürlich hängt das alles von Ihrem Projekt ab, aber in der Praxis ist es nicht selten, dass ein Spiel eine große Anzahl von GameObjects in der Szene verwendet, von denen jedes in jedem Frame eine bestimmte Logik ausführt. Normalerweise handelt es sich dabei um einen kleinen Teil des Codes, der keine Auswirkungen zu haben scheint, aber wenn die Anzahl sehr groß wird, macht sich der Overhead durch den Aufruf von Tausenden von Update-Methoden bemerkbar. Zu diesem Zeitpunkt könnte es bereits zu spät sein, die Architektur des Spiels zu ändern und all diese Objekte in ein Manager-Muster umzuwandeln.
Sie haben die Daten jetzt, denken Sie zu Beginn Ihres nächsten Projekts daran.
