Code-Optimierung

Im Allgemeinen sollten Sie Ihren Code zuerst ohne Optimierungen kompilieren und ausführen, was der Standard ist, wenn Sie emcc ohne Angabe eines Optimierungslevels ausführen. Solche unoptimierten Builds enthalten einige Prüfungen und Zusicherungen, die sehr hilfreich sein können, um sicherzustellen, dass Ihr Code korrekt läuft. Sobald dies der Fall ist, wird dringend empfohlen, die Builds, die Sie ausliefern, aus mehreren Gründen zu optimieren: Erstens sind optimierte Builds viel kleiner und schneller, sodass sie schnell geladen werden und reibungsloser laufen, und zweitens enthalten un-optimierte Builds Debug-Informationen wie Datei- und Funktionsnamen, Code-Kommentare in JavaScript usw. (was abgesehen von der Größensteigerung auch Dinge enthalten kann, die Sie Ihren Benutzern nicht ausliefern möchten).

Der Rest dieser Seite erklärt, wie Sie Ihren Code optimieren können.

Wie man Code optimiert

Code wird durch Angabe von Optimierungs-Flags beim Ausführen von emcc optimiert. Die Level umfassen: -O0 (keine Optimierung), -O1, -O2, -Os, -Oz, -Og und -O3.

Zum Beispiel, um mit Optimierungslevel -O2 zu kompilieren

emcc -O2 file.cpp

Die höheren Optimierungslevel führen zunehmend aggressivere Optimierungen ein, was zu verbesserter Leistung und geringerer Codegröße auf Kosten längerer Kompilierungszeiten führt. Die Level können auch verschiedene Probleme im Zusammenhang mit undefiniertem Verhalten im Code aufzeigen.

Das Optimierungslevel, das Sie verwenden sollten, hängt hauptsächlich vom aktuellen Entwicklungsstadium ab

  • Wenn Sie Code zum ersten Mal portieren, führen Sie *emcc* mit den Standardeinstellungen (ohne Optimierung) für Ihren Code aus. Überprüfen Sie, ob Ihr Code funktioniert, und debuggen und beheben Sie alle Probleme, bevor Sie fortfahren.

  • Erstellen Sie während der Entwicklung mit niedrigeren Optimierungsleveln für einen kürzeren Kompilierungs-/Test-Iterationszyklus (-O0 oder -O1).

  • Erstellen Sie mit -O2, um einen gut optimierten Build zu erhalten.

  • Das Erstellen mit -O3 oder -Os kann einen noch besseren Build als -O2 erzeugen und ist für Release-Builds eine Überlegung wert. -O3-Builds sind noch stärker optimiert als -O2, jedoch auf Kosten einer deutlich längeren Kompilierungszeit und potenziell größeren Codegröße. -Os ähnelt in der Verlängerung der Kompilierungszeiten, konzentriert sich jedoch auf die Reduzierung der Codegröße bei zusätzlichen Optimierungen. Es lohnt sich, diese verschiedenen Optimierungsoptionen auszuprobieren, um zu sehen, was für Ihre Anwendung am besten funktioniert.

  • Weitere Optimierungen werden in den folgenden Abschnitten erörtert.

Hinweis

  • Die Bedeutung der *emcc*-Optimierungs-Flags (-O1, -O2 usw.) ähnelt der von *gcc*, *clang* und anderen Compilern, unterscheidet sich aber auch, da die Optimierung von WebAssembly einige zusätzliche Arten von Optimierungen umfasst. Die Zuordnung der *emcc*-Level zu den LLVM-Bitcode-Optimierungsleveln ist in der Referenz dokumentiert.

Wie Emscripten optimiert

Das Kompilieren von Quelldateien zu Objektdateien funktioniert, wie Sie es von einem nativen Build-System erwarten würden, das clang und LLVM verwendet. Beim Verlinken von Objektdateien zum endgültigen ausführbaren Programm führt Emscripten je nach Optimierungslevel zusätzliche Optimierungen durch

  • Der Binaryen-Optimierer wird ausgeführt. Binaryen führt sowohl allgemeine Optimierungen an Wasm durch, die LLVM nicht vornimmt, als auch einige Ganzprogramm-Optimierungen. (Beachten Sie, dass Binaryens Ganzprogramm-Optimierungen Dinge wie Inlining tun können, was in einigen Fällen überraschend sein kann, da LLVM IR-Attribute wie noinline an dieser Stelle verloren gegangen sind.)

  • JavaScript wird in dieser Phase generiert und vom Emscripten JS-Optimierer optimiert. Optional können Sie auch den Closure-Compiler ausführen, der für die Codegröße dringend empfohlen wird.

  • Emscripten optimiert auch das kombinierte Wasm+JS, indem es Importe und Exporte zwischen ihnen minimiert und Meta-DCE ausführt, das ungenutzten Code in Zyklen entfernt, die die beiden Welten umfassen.

Erweiterte Compiler-Einstellungen

Es gibt mehrere Flags, die Sie an den Compiler übergeben können, um die Codegenerierung zu beeinflussen, was sich auch auf die Leistung auswirkt – zum Beispiel DISABLE_EXCEPTION_CATCHING. Diese sind in src/settings.js dokumentiert.

WebAssembly

Emscripten gibt standardmäßig WebAssembly aus. Sie können dies mit -sWASM=0 deaktivieren (in diesem Fall gibt Emscripten JavaScript aus), was notwendig ist, wenn die Ausgabe an Orten ausgeführt werden soll, an denen Wasm-Unterstützung noch nicht vorhanden ist, aber der Nachteil ist größerer und langsamerer Code.

Codegröße

Dieser Abschnitt beschreibt Optimierungen und Probleme, die für die Codegröße relevant sind. Sie sind sowohl für kleine Projekte oder Bibliotheken nützlich, bei denen Sie den kleinstmöglichen Footprint erzielen möchten, als auch für große Projekte, bei denen die schiere Größe Probleme (wie langsame Startgeschwindigkeit) verursachen kann, die Sie vermeiden möchten.

Abwägung zwischen Codegröße und Leistung

Sie möchten möglicherweise die weniger leistungsempfindlichen Quelldateien in Ihrem Projekt mit -Os oder -Oz und den Rest mit -O2 erstellen (-Os und -Oz ähneln -O2, reduzieren jedoch die Codegröße auf Kosten der Leistung. -Oz reduziert die Codegröße stärker als -Os.)

Separates können Sie den finalen Link-/Build-Befehl mit -Os oder -Oz ausführen, damit der Compiler bei der Generierung des WebAssembly-Moduls stärker auf die Codegröße achtet.

Verschiedene Tipps zur Codegröße

Zusätzlich zu den oben genannten Punkten können die folgenden Tipps dazu beitragen, die Codegröße zu reduzieren

  • Verwenden Sie den Closure-Compiler für den nicht-kompilierten Code: --closure 1. Dies kann die Größe des unterstützenden JavaScript-Codes erheblich reduzieren und wird dringend empfohlen. Wenn Sie jedoch Ihren eigenen zusätzlichen JavaScript-Code hinzufügen (z. B. in einem --pre-js), müssen Sie sicherstellen, dass er Closure-Annotationen korrekt verwendet.

  • Flohs Blogbeitrag zu diesem Thema ist sehr hilfreich.

  • Stellen Sie sicher, dass Sie die Gzip-Kompression auf Ihrem Webserver verwenden, die alle Browser jetzt unterstützen.

Die folgenden Compiler-Einstellungen können hilfreich sein (siehe src/settings.js für weitere Details)

  • Deaktivieren Sie Inlining, wenn möglich, mithilfe von -sINLINING_LIMIT. Das Kompilieren mit -Os oder -Oz vermeidet im Allgemeinen auch Inlining. (Inlining kann den Code jedoch schneller machen, also mit Vorsicht verwenden.)

  • Sie können die Option -sFILESYSTEM=0 verwenden, um die Bündelung von Dateisystem-Unterstützungscode zu deaktivieren (der Compiler sollte ihn optimieren, wenn er nicht verwendet wird, aber dies gelingt möglicherweise nicht immer). Dies kann nützlich sein, wenn Sie beispielsweise eine reine Berechnungsbibliothek erstellen.

  • Das ENVIRONMENT-Flag ermöglicht es Ihnen anzugeben, dass die Ausgabe nur im Web oder nur in node.js usw. ausgeführt wird. Dies verhindert, dass der Compiler Code zur Unterstützung aller möglichen Laufzeitumgebungen ausgibt, was ~2KB spart.

LTO

Link Time Optimization (LTO) ermöglicht dem Compiler mehr Optimierungen, da er über separate Kompilierungseinheiten hinweg und sogar mit Systembibliotheken inlining kann. LTO wird durch Kompilieren von Objektdateien mit -flto aktiviert. Die Wirkung dieses Flags ist die Ausgabe von LTO-Objektdateien (technisch bedeutet dies die Ausgabe von Bitcode). Der Linker kann eine Mischung aus Wasm-Objektdateien und LTO-Objektdateien verarbeiten. Das Übergeben von -flto zur Linkzeit löst auch die Verwendung von LTO-Systembibliotheken aus.

Um somit maximale LTO-Möglichkeiten mit dem LLVM Wasm-Backend zu ermöglichen, erstellen Sie alle Quelldateien mit -flto und verknüpfen Sie auch mit flto.

EVAL_CTORS

Das Erstellen mit -sEVAL_CTORS wird so viel Code wie möglich zur Kompilierzeit auswerten. Das umfasst sowohl die "global ctor"-Funktionen (Funktionen, die LLVM ausgibt und die vor main() ausgeführt werden) als auch main() selbst. So viel wie möglich wird ausgewertet, und der resultierende Zustand wird dann in das Wasm "gespeichert". Wenn das Programm dann ausgeführt wird, beginnt es in diesem Zustand und muss diesen Code nicht ausführen, was Zeit sparen kann.

Diese Optimierung kann die Codegröße entweder reduzieren oder erhöhen. Wenn zum Beispiel eine kleine Menge Code viele Änderungen im Speicher generiert, kann die Gesamtgröße zunehmen. Es ist am besten, mit diesem Flag zu bauen und dann Code und Startgeschwindigkeit zu messen, um zu sehen, ob der Kompromiss in Ihrem Programm lohnenswert ist.

Sie können sich bemühen, EVAL_CTORS-freundlichen Code zu schreiben, indem Sie Dinge, die nicht ausgewertet werden können, so weit wie möglich aufschieben. Zum Beispiel unterbrechen Aufrufe an Importe diese Optimierung, und wenn Sie eine Spiel-Engine haben, die einen GL-Kontext erstellt und dann einige reine Berechnungen durchführt, um unabhängige Datenstrukturen im Speicher einzurichten, könnten Sie diese Reihenfolge umkehren. Dann könnten die reinen Berechnungen zuerst ausgeführt und weggewertet werden, und der GL-Kontext-Erstellungsaufruf an einen Import würde dies nicht verhindern. Andere Dinge, die Sie tun können, sind die Vermeidung von argc/argv, die Vermeidung von getenv() und so weiter.

Beim Verwenden dieser Option wird eine Protokollierung angezeigt, damit Sie sehen können, ob Verbesserungen vorgenommen werden können. Hier ist ein Beispiel für die Ausgabe von emcc -sEVAL_CTORS

trying to eval __wasm_call_ctors
  ...partial evalling successful, but stopping since could not eval: call import: wasi_snapshot_preview1.environ_sizes_get
       recommendation: consider --ignore-external-input
  ...stopping

Die erste Zeile zeigt den Versuch, die LLVM-Funktion auszuwerten, die globale Konstruktoren ausführt. Sie hat einen Teil der Funktion ausgewertet, stoppte dann aber beim WASI-Import environ_sizes_get, was bedeutet, dass sie versucht, aus der Umgebung zu lesen. Wie die Ausgabe besagt, können Sie EVAL_CTORS anweisen, externe Eingaben zu ignorieren, was solche Dinge ignoriert. Sie können dies mit Modus 2 aktivieren, d.h. mit emcc -sEVAL_CTORS=2 erstellen

trying to eval __wasm_call_ctors
  ...success on __wasm_call_ctors.
trying to eval main
  ...stopping (in block) since could not eval: call import: wasi_snapshot_preview1.fd_write
  ...stopping

Jetzt wurde __wasm_call_ctors vollständig ausgewertet. Dann ging es weiter zu main, wo es aufgrund eines Aufrufs von WASIs fd_write, d.h. eines Aufrufs zum Ausdrucken von etwas, stoppte.

Sehr große Codebasen

Der vorherige Abschnitt zur Reduzierung der Codegröße kann bei sehr großen Codebasen hilfreich sein. Darüber hinaus sind hier einige weitere Themen, die nützlich sein könnten.

Alleine ausführen

Wenn Sie in Browsern auf Speichergrenzen stoßen, kann es hilfreich sein, Ihr Projekt alleine auszuführen, anstatt es in einer Webseite mit anderem Inhalt. Wenn Sie eine neue Webseite (als neuer Tab oder neues Fenster) öffnen, die nur Ihr Projekt enthält, haben Sie die besten Chancen, Speicherfragmentierungsprobleme zu vermeiden.

Modulaufteilung

Wenn Ihr Modul groß genug ist, dass die Download- und Instanziierungszeit die Startleistung Ihrer Anwendung merklich beeinflusst, kann es sich lohnen, das Modul aufzuteilen und das Laden von Code, der für den Start der Anwendung nicht notwendig ist, zu verzögern. Eine Anleitung dazu finden Sie unter Modulaufteilung. *Beachten Sie, dass die Modulaufteilung ein experimentelles Feature ist und sich ändern kann.*

Weitere Optimierungsprobleme

C++ Ausnahmen

Das Abfangen von C++-Ausnahmen (insbesondere das Emittieren von Catch-Blöcken) ist standardmäßig in -O1 (und höher) deaktiviert. Aufgrund der Art und Weise, wie WebAssembly derzeit Ausnahmen implementiert, macht dies den Code viel kleiner und schneller (eventuell sollte Wasm native Unterstützung für Ausnahmen erhalten und dieses Problem nicht mehr haben).

Um Ausnahmen in optimiertem Code wieder zu aktivieren, führen Sie *emcc* mit -sDISABLE_EXCEPTION_CATCHING=0 aus (siehe src/settings.js).

Hinweis

Wenn die Ausnahmebehandlung deaktiviert ist, beendet eine geworfene Ausnahme die Anwendung. Mit anderen Worten, eine Ausnahme wird immer noch geworfen, aber nicht gefangen.

Hinweis

Selbst wenn keine Catch-Blöcke generiert werden, gibt es einen gewissen Overhead in der Codegröße, es sei denn, Sie erstellen Ihre Quelldateien mit -fno-exceptions, wodurch der gesamte Code zur Ausnahmeunterstützung weggelassen wird (z. B. wird vermieden, in Fehlern in std::vector ordnungsgemäße C++-Ausnahmeobjekte zu erstellen, und die Anwendung wird einfach abgebrochen, wenn sie auftreten).

C++ RTTI

Die C++-Laufzeittypinformationsunterstützung (dynamische Umwandlungen usw.) fügt Overhead hinzu, der manchmal nicht benötigt wird. Zum Beispiel werden in Box2D weder RTTI noch Ausnahmen benötigt, und wenn Sie die Quelldateien mit -fno-rtti -fno-exceptions erstellen, verkleinert dies die Ausgabe um 15 % (!).

Speicherwachstum

Das Erstellen mit -sALLOW_MEMORY_GROWTH ermöglicht es, die insgesamt verwendete Speichermenge je nach den Anforderungen der Anwendung zu ändern. Dies ist nützlich für Apps, die nicht im Voraus wissen, wie viel sie benötigen werden.

Code-Optimierungspässe anzeigen

Aktivieren Sie den Debug-Modus (EMCC_DEBUG), um Dateien für jede Kompilierungsphase auszugeben, einschließlich der wichtigsten Optimierungsoperationen.

Zuweisung

Die standardmäßige Implementierung von malloc/free ist dlmalloc. Sie können auch emmalloc (-sMALLOC=emmalloc) wählen, das kleiner, aber weniger schnell ist, oder mimalloc (-sMALLOC=mimalloc), das größer ist, aber in einer Multithread-Anwendung mit Konflikten bei malloc/free besser skaliert (siehe Allocator performance).

Unsichere Optimierungen

Einige UNSICHERE Optimierungen, die Sie vielleicht ausprobieren möchten, sind

  • --closure 1: Dies kann helfen, die Größe des nicht-generierten (Support-/Glue-) JS-Codes zu reduzieren und den Start zu beschleunigen. Es kann jedoch zu Problemen führen, wenn Sie keine korrekten Closure Compiler-Annotationen und Exporte verwenden. Aber es lohnt sich!

Profilierung

Moderne Browser verfügen über JavaScript-Profiler, die dabei helfen können, die langsameren Teile Ihres Codes zu finden. Da jeder Browser-Profiler Einschränkungen hat, wird dringend empfohlen, in mehreren Browsern zu profilieren.

Um sicherzustellen, dass kompilierter Code genügend Informationen für die Profilierung enthält, erstellen Sie Ihr Projekt mit Profilierung sowie Optimierung und anderen Flags

emcc -O2 --profiling file.cpp

Fehlerbehebung bei schlechter Leistung

Emscripten-kompilierter Code kann oft der Geschwindigkeit eines nativen Builds nahe kommen. Wenn die Leistung deutlich schlechter als erwartet ist, können Sie auch die folgenden zusätzlichen Schritte zur Fehlerbehebung durchführen

  • Das Erstellen von Projekten ist ein zweistufiger Prozess: Kompilieren von Quellcodedateien nach LLVM und Generieren von JavaScript aus LLVM. Haben Sie in beiden Schritten mit denselben Optimierungswerten (-O2 oder -O3) erstellt?

  • Auf mehreren Browsern testen. Wenn die Leistung in einem Browser akzeptabel ist und in einem anderen deutlich schlechter, dann melden Sie einen Fehler und geben Sie den problematischen Browser sowie weitere relevante Informationen an.