Engine & platform

Optimierung der Ladeleistung: Verstehen der Async-Upload-Pipeline

JOSEPH SCHEINBERG / UNITY TECHNOLOGIESContributor
Oct 8, 2018|7 Min.
Optimierung der Ladeleistung: Verstehen der Async-Upload-Pipeline
Diese Website wurde aus praktischen Gründen für Sie maschinell übersetzt. Die Richtigkeit und Zuverlässigkeit des übersetzten Inhalts kann von uns nicht gewährleistet werden. Sollten Sie Zweifel an der Richtigkeit des übersetzten Inhalts haben, schauen Sie sich bitte die offizielle englische Version der Website an.

Niemand mag Ladebildschirme. Wussten Sie, dass Sie die Parameter der Async Upload Pipeline (AUP) schnell anpassen können, um Ihre Ladezeiten erheblich zu verbessern? Dieser Artikel beschreibt, wie Meshes und Texturen über die AUP geladen werden. Mit diesem Wissen können Sie die Ladezeit erheblich beschleunigen - bei einigen Projekten wurde eine Leistungssteigerung um mehr als das Doppelte festgestellt!

Lesen Sie weiter, um zu erfahren, wie das AUP aus technischer Sicht funktioniert und welche APIs Sie verwenden sollten, um den größtmöglichen Nutzen daraus zu ziehen.

Probieren Sie es aus

Die neueste, optimale Implementierung der Asset-Upload-Pipeline ist in der Beta-Version 2018.3 verfügbar.

2018.3 Beta heute herunterladen

Lassen Sie uns zunächst einen detaillierten Blick darauf werfen, wann die AUP verwendet wird und wie der Ladevorgang funktioniert.

Wann wird die Async Upload Pipeline verwendet?

Vor 2018.3 war die AUP nur für Texturen zuständig. Ab 2018.3 Beta lädt die AUP nun Texturen und Meshes, aber es gibt einige Ausnahmen. Texturen, die zum Lesen/Schreiben aktiviert sind, oder Meshes, die zum Lesen/Schreiben aktiviert oder komprimiert sind, verwenden die AUP nicht. (Beachten Sie, dass das mit 2018.2 eingeführte Texture Mipmap Streaming auch AUP verwendet).

Wie der Ladevorgang funktioniert

Während des Erstellungsprozesses wird das Textur- oder Mesh-Objekt in eine serialisierte Datei geschrieben und die großen Binärdaten (Textur- oder Vertexdaten) werden in eine begleitende .resS-Datei geschrieben. Dieses Layout gilt sowohl für Playerdaten als auch für Asset-Bundles. Die Trennung von Objekt- und Binärdaten ermöglicht ein schnelleres Laden der serialisierten Datei (die in der Regel kleine Objekte enthält) und ein rationelleres Laden der großen Binärdaten aus der .resS-Datei. Wenn das Textur- oder Mesh-Objekt deserialisiert wird, sendet es einen Befehl an die Befehlswarteschlange des AUP. Sobald dieser Befehl abgeschlossen ist, wurden die Textur- oder Mesh-Daten auf die GPU hochgeladen und das Objekt kann in den Hauptthread integriert werden.

Sobald dieser Befehl abgeschlossen ist, wurden die Textur- oder Netzdaten auf die GPU hochgeladen und das Objekt kann in den Hauptthread integriert werden

Während des Hochladevorgangs werden die großen Binärdaten aus der .resS-Datei in einen Ringspeicher fester Größe eingelesen. Sobald die Daten im Speicher sind, werden sie im Rendering-Thread zeitlich gestaffelt auf die GPU hochgeladen. Die Größe des Ringpuffers und die Dauer der Zeitscheibe sind die beiden Parameter, die Sie ändern können, um das Verhalten des Systems zu beeinflussen.

Die Async-Upload-Pipeline hat für jeden Befehl den folgenden Ablauf:

1. Warten Sie, bis der erforderliche Speicherplatz im Ringpuffer verfügbar ist.

2. Lesen von Daten aus der .resS-Quelldatei in den zugewiesenen Speicher.

3. Nachbearbeitung (Dekomprimierung der Texturen, Erzeugung von Mesh-Kollisionen, Korrekturen pro Plattform usw.).

4. Zeitlich gestaffeltes Hochladen auf den Render-Thread

5. Ringpufferspeicher freigeben.

Es können mehrere Befehle gleichzeitig ausgeführt werden, aber alle müssen ihren benötigten Speicher aus demselben gemeinsamen Ringpuffer zuweisen. Wenn der Ringpuffer voll ist, warten neue Befehle; dieses Warten führt nicht zum Blockieren des Hauptthreads oder beeinträchtigt die Bildrate, sondern verlangsamt lediglich den asynchronen Ladevorgang.

Eine Zusammenfassung dieser Auswirkungen ist im Folgenden dargestellt:

Bild
Welche öffentlichen APIs sind verfügbar, um Ladeparameter anzupassen?

Um die Vorteile des AUP in 2018.3 voll auszuschöpfen, gibt es drei Parameter, die zur Laufzeit für dieses System angepasst werden können:

  • QualitySettings.asyncUploadTimeSlice - Die Zeit in Millisekunden, die für das Hochladen von Texturen und Mesh-Daten auf dem Render-Thread für jedes Bild benötigt wird. Wenn ein asynchroner Ladevorgang läuft, führt das System zwei Zeitscheiben dieser Größe aus. Der Standardwert ist 2ms. Wenn dieser Wert zu klein ist, kann es zu Engpässen beim Hochladen von Texturen und Netzen auf die GPU kommen. Ein zu großer Wert hingegen kann zu Framerate-Einbrüchen führen.
  • QualitySettings.asyncUploadBufferSize - Die Größe des Ringpuffers in Megabyte. Wenn die Upload-Zeitscheibe bei jedem Frame auftritt, wollen wir sicher sein, dass wir genügend Daten im Ringpuffer haben, um die gesamte Zeitscheibe zu nutzen. Wenn der Ringspeicher zu klein ist, wird der Upload-Zeitabschnitt verkürzt. Der Standardwert betrug 4 MB in 2018.2, wurde aber in 2018.3 auf 16 MB erhöht.
  • QualitySettings.asyncUploadPersistentBuffer - Eingeführt in 2018.3, bestimmt dieses Flag, ob der Upload-Ringpuffer freigegeben wird, wenn alle anstehenden Lesevorgänge abgeschlossen sind. Das Zuweisen und Freigeben dieses Puffers kann oft zu einer Fragmentierung des Speichers führen, daher sollte er im Allgemeinen auf seinem Standardwert (true) belassen werden. Wenn Sie wirklich Speicher zurückgewinnen müssen, wenn Sie nicht laden, können Sie diesen Wert auf false setzen.

Diese Einstellungen können über die Skripting-API oder über das Menü QualitySettings angepasst werden.

Bild von Async Upload Persistent Buffer ausgewählt
Beispiel-Workflow

Untersuchen wir einen Workload mit vielen Texturen und Meshes, die über die Async Upload Pipeline hochgeladen werden, wobei die Standardzeitscheibe von 2 ms und ein 4 MB großer Ringpuffer verwendet werden. Da wir laden, bekommen wir 2 Zeitscheiben pro Renderframe, also sollten wir 4 Millisekunden Uploadzeit haben. Ein Blick auf die Profiler-Daten zeigt, dass wir nur etwa 1,5 Millisekunden benötigen. Wir können auch sehen, dass unmittelbar nach dem Hochladen ein neuer Lesevorgang durchgeführt wird, da nun Speicher im Ringpuffer verfügbar ist. Dies ist ein Zeichen dafür, dass ein größerer Ringpuffer erforderlich ist.

Beispiel einer nach dem Hochladen gelesenen Datei

Versuchen wir, den Ringpuffer zu erhöhen, und da wir uns in einem Ladebildschirm befinden, ist es auch eine gute Idee, die Upload-Zeitscheibe zu erhöhen. So sieht ein 16-MB-Ringpuffer und ein 4-Millisekunden-Zeitfenster aus:

Abbildung eines 16-MB-Ringpuffers und eines 4-Millisekunden-Zeitfensters:

Jetzt können wir sehen, dass wir fast die gesamte Zeit unseres Render-Threads mit dem Hochladen verbringen und nur eine kurze Zeit zwischen den Uploads mit dem Rendern des Bildes.

Nachfolgend sind die Ladezeiten des Beispiel-Workloads mit verschiedenen Upload-Zeitabschnitten und Ringpuffergrößen aufgeführt. Die Tests wurden auf einem MacBook Pro, 2,8 GHz Intel Core i7 mit OS X El Capitan durchgeführt. Die Upload- und E/A-Geschwindigkeiten variieren je nach Plattform und Gerät. Der Workload ist eine Teilmenge des Viking Village-Beispielprojekts, das wir intern für Leistungstests verwenden. Da noch andere Objekte geladen werden, können wir den genauen Leistungsgewinn der verschiedenen Werte nicht ermitteln. In diesem Fall kann man jedoch mit Sicherheit sagen, dass das Laden von Texturen und Meshes mindestens doppelt so schnell ist, wenn man von den 4MB/2MS-Einstellungen zu den 16MB/4MS-Einstellungen wechselt.

Das Experimentieren mit diesen Parametern führt zu den folgenden Ergebnissen.

I'mage der durchschnittlichen Ladezeiten

Um die Ladezeiten für dieses spezielle Beispielprojekt zu optimieren, sollten wir daher die folgenden Einstellungen vornehmen:

Unbekannter Blocktyp "codeBlock", bitte geben Sie einen Serializer dafür in der `serializers.types` prop an

Schlussfolgerungen und Empfehlungen

Allgemeine Empfehlungen zur Optimierung der Ladegeschwindigkeit von Texturen und Meshes:

  • Wählen Sie die größte QualitySettings.asyncUploadTimeSlice, die nicht zum Fallenlassen von Bildern führt.
  • Erhöhen Sie während der Ladebildschirme vorübergehend QualitySettings.asyncUploadTimeSlice.
  • Verwenden Sie den Profiler, um die Nutzung der Zeitscheiben zu untersuchen. Die Zeitscheibe wird im Profiler als AsyncUploadManager.AsyncResourceUpload angezeigt. Erhöhen Sie QualitySettings.asyncUploadBufferSize, wenn Ihre Zeitscheibe nicht vollständig genutzt wird.
  • Mit einer größeren QualitySettings.asyncUploadBufferSize werden die Dateien im Allgemeinen schneller geladen. Wenn Sie es sich leisten können, sollten Sie den Speicher auf 16 oder 32 MB erhöhen.
  • Lassen Sie QualitySettings.asyncUploadPersistentBuffer auf true gesetzt, es sei denn, Sie haben einen zwingenden Grund, die Speichernutzung während der Laufzeit zu reduzieren, wenn Sie nicht laden.
FAQ

Q: Wie oft werden zeitlich gestaffelte Uploads im Rendering-Thread durchgeführt?

  • Das zeitlich gestaffelte Hochladen erfolgt einmal pro Renderframe oder zweimal während eines asynchronen Ladevorgangs. VSync wirkt sich auf diese Pipeline aus. Während der Rendering-Thread auf eine VSync wartet, könnten Sie etwas hochladen. Wenn Sie mit 16 ms Frames arbeiten und ein Frame länger dauert, z. B. 17 ms, müssen Sie am Ende 15 ms auf die Vsync warten. Im Allgemeinen gilt: Je höher die Bildrate, desto häufiger treten Upload-Zeitscheiben auf.

Q: Was wird durch die AUP geladen?

  • Texturen, die nicht lese-/schreibfähig sind, werden über das AUP hochgeladen.
  • Ab 2018.2 werden Textur-Mipmaps durch die AUP gestreamt.
  • Ab 2018.3 werden Meshes auch über das AUP hochgeladen, sofern sie unkomprimiert und nicht schreib- und lesefähig sind.

Q: Was ist, wenn der Ringpuffer nicht groß genug ist, um die hochgeladenen Daten aufzunehmen (z. B. eine sehr große Textur)?

  • Upload-Befehle, die größer als der Ringpuffer sind, warten, bis der Ringpuffer vollständig aufgebraucht ist, dann wird der Ringpuffer neu zugewiesen, um die große Zuweisung zu erfüllen. Sobald der Upload abgeschlossen ist, wird der Ringspeicher wieder in seiner ursprünglichen Größe zugewiesen.

Q: Wie funktionieren synchrone Lade-APIs? Zum Beispiel Resources.Load, AssetBundle.LoadAsset, usw.

  • Synchrone Ladeaufrufe verwenden die AUP und blockieren im Wesentlichen den Hauptthread, bis der asynchrone Ladevorgang abgeschlossen ist. Die Art der verwendeten Lade-API ist nicht relevant.
Sagen Sie uns, was Sie denken

Wir sind immer auf der Suche nach Feedback. Teilen Sie uns Ihre Meinung in den Kommentaren oder im Unity 2018.3 Beta-Forum mit!