Home
» 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.
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.
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.
Um zusätzliche Optimierungsarbeit zur Linkzeit zu überspringen, verknüpfen Sie mit -O0 oder -O1. In diesen Modi konzentriert sich Emscripten auf schnellere Iterationszeiten. (Beachten Sie, dass es in Ordnung ist, mit diesen Flags zu verknüpfen, auch wenn die Quelldateien mit einem anderen Optimierungslevel kompiliert wurden.)
Um auch Nicht-Optimierungsarbeiten zur Linkzeit zu überspringen, verknüpfen Sie mit -sWASM_BIGINT. Die Aktivierung der BigInt-Unterstützung macht es für Emscripten unnötig, das Wasm zu „legalisieren“, um i64-Werte an der JS/Wasm-Grenze zu handhaben (da mit BigInts i64-Werte legal sind und keine zusätzliche Verarbeitung erfordern).
Einige Link-Flags fügen im Link-Schritt zusätzliche Arbeit hinzu, die die Dinge verlangsamen kann. Zum Beispiel aktiviert -g DWARF-Unterstützung, Flags wie -sSAFE_HEAP erfordern JS-Nachbearbeitung, und Flags wie -sASYNCIFY erfordern Wasm-Nachbearbeitung. Um sicherzustellen, dass Ihre Flags die schnellstmögliche Verknüpfung ermöglichen, bei der das Wasm nach wasm-ld nicht geändert wird, erstellen Sie mit -sERROR_ON_WASM_CHANGES_AFTER_LINK. Mit dieser Option erhalten Sie während des Verknüpfungsvorgangs einen Fehler, wenn Emscripten Änderungen am Wasm vornehmen muss. Wenn Sie beispielsweise -sWASM_BIGINT nicht übergeben haben, wird Ihnen mitgeteilt, dass die Legalisierung eine Änderung des Wasm erzwingt. Sie erhalten auch einen Fehler, wenn Sie mit -O2 oder höher erstellen, da der Binaryen-Optimierer normalerweise ausgeführt würde.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.*
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).
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 % (!).
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.
Aktivieren Sie den Debug-Modus (EMCC_DEBUG), um Dateien für jede Kompilierungsphase auszugeben, einschließlich der wichtigsten Optimierungsoperationen.
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).
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!
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
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.