Das Android Runtime (ART)-Team hat die Kompilierungszeit um 18% reduziert, ohne den kompilierten Code oder Spitzenwerte des Arbeitsspeichers zu beeinträchtigen. Diese Verbesserung war Teil unserer Initiative für 2025, die Kompilierungszeit zu verkürzen, ohne die Arbeitsspeichernutzung oder die Qualität des kompilierten Codes zu beeinträchtigen.
Die Optimierung der Kompilierungsgeschwindigkeit ist für ART von entscheidender Bedeutung. Wenn beispielsweise eine Just-in-Time-Kompilierung (JIT) erfolgt, wirkt sich das direkt auf die Effizienz von Anwendungen und die Gesamtleistung des Geräts aus. Durch schnellere Kompilierungen wird die Zeit verkürzt, bis die Optimierungen wirksam werden. Das führt zu einer flüssigeren und reaktionsschnelleren Nutzererfahrung. Sowohl bei JIT als auch bei AOT führt eine höhere Kompilierungsgeschwindigkeit zu einem geringeren Ressourcenverbrauch während des Kompilierungsprozesses, was sich positiv auf die Akkulaufzeit und die Wärmeentwicklung des Geräts auswirkt, insbesondere bei Geräten der unteren Preisklasse.
Einige dieser Verbesserungen der Kompilierungsgeschwindigkeit wurden im Juni 2025 in Android eingeführt. Die restlichen Verbesserungen werden in der Android-Version zum Jahresende verfügbar sein. Außerdem können alle Android-Nutzer mit Version 12 und höher diese Verbesserungen über Mainline-Updates erhalten.
Optimierungs-Compiler optimieren
Die Optimierung eines Compilers ist immer ein Abwägen von Kompromissen. Geschwindigkeit gibt es nicht kostenlos. Sie müssen etwas dafür aufgeben. Wir haben uns ein sehr klares und anspruchsvolles Ziel gesetzt: den Compiler schneller zu machen, ohne dass es zu Speicherregressionen kommt und vor allem ohne die Qualität des generierten Codes zu beeinträchtigen. Wenn der Compiler schneller ist, die Apps aber langsamer ausgeführt werden, haben wir unser Ziel nicht erreicht.
Die einzige Ressource, die wir bereit waren zu investieren, war unsere eigene Entwicklungszeit, um uns intensiv mit dem Thema zu befassen, es zu untersuchen und clevere Lösungen zu finden, die diese strengen Kriterien erfüllen. Sehen wir uns genauer an, wie wir nach Verbesserungsmöglichkeiten suchen und die richtigen Lösungen für die verschiedenen Probleme finden.
Sinnvolle mögliche Optimierungen finden
Bevor Sie einen Messwert optimieren können, müssen Sie ihn messen können. Andernfalls können Sie nie sicher sein, ob Sie es verbessert haben oder nicht. Glücklicherweise ist die Kompilierungszeit relativ konstant, solange Sie einige Vorsichtsmaßnahmen treffen, z. B. dasselbe Gerät für die Messung vor und nach einer Änderung verwenden und darauf achten, dass Ihr Gerät nicht durch Überhitzung gedrosselt wird. Außerdem haben wir deterministische Messungen wie Compilerstatistiken, die uns helfen, die Vorgänge im Hintergrund zu verstehen.
Da wir für diese Verbesserungen Entwicklungszeit opfern mussten, wollten wir so schnell wie möglich iterieren. Dazu haben wir eine Reihe repräsentativer Apps (eine Mischung aus eigenen Apps, Drittanbieter-Apps und dem Android-Betriebssystem selbst) ausgewählt, um Lösungen zu entwickeln. Später haben wir durch manuelle und automatisierte Tests überprüft, ob sich die endgültige Implementierung gelohnt hat.
Mit dieser Auswahl an APKs haben wir lokal eine manuelle Kompilierung ausgelöst, ein Profil der Kompilierung erstellt und mit pprof visualisiert, wo wir unsere Zeit verbringen.
Beispiel für ein Flame-Diagramm eines Profils in pprof
Das pprof-Tool ist sehr leistungsstark und ermöglicht es uns, die Daten zu segmentieren, zu filtern und zu sortieren, um beispielsweise zu sehen, welche Compilerphasen oder Methoden die meiste Zeit in Anspruch nehmen. Wir werden nicht auf pprof selbst eingehen. Wenn der Balken größer ist, hat die Kompilierung mehr Zeit in Anspruch genommen.
Eine dieser Ansichten ist die „Bottom-up“-Ansicht, in der Sie sehen können, welche Methoden am meisten Zeit in Anspruch nehmen. Im Bild unten sehen wir eine Methode namens „Kill“, die für über 1% der Kompilierungszeit verantwortlich ist. Einige der anderen Top-Methoden werden später im Blogpost ebenfalls behandelt.
Ansicht eines Profils von unten
In unserem optimierenden Compiler gibt es eine Phase namens „Global Value Numbering“ (GVN). Sie müssen sich keine Gedanken darüber machen, was es als Ganzes tut. Wichtig ist nur, dass es eine Methode namens „Kill“ hat, mit der einige Knoten gemäß einem Filter gelöscht werden. Das ist zeitaufwendig, da alle Knoten durchlaufen und einzeln geprüft werden müssen. Wir haben festgestellt, dass es einige Fälle gibt, in denen wir im Voraus wissen, dass die Prüfung falsch sein wird, unabhängig davon, welche Knoten zu diesem Zeitpunkt aktiv sind. In diesen Fällen können wir die Iteration ganz überspringen, wodurch die Anzahl der Knoten von 1,023% auf etwa 0,3% sinkt und die Laufzeit von GVN um etwa 15 % verkürzt wird.
Sinnvolle Optimierungen implementieren
Wir haben uns angesehen, wie Sie messen und erkennen können, wo die Zeit verbracht wird. Das ist aber erst der Anfang. Im nächsten Schritt geht es darum, wie Sie die für die Kompilierung benötigte Zeit optimieren können.
In einem Fall wie dem oben genannten „Kill“ würden wir uns normalerweise ansehen, wie wir die Knoten durchlaufen und dies beschleunigen können, z. B. durch parallele Verarbeitung oder Verbesserung des Algorithmus selbst. Tatsächlich haben wir das zuerst versucht. Erst als wir nichts mehr tun konnten, hatten wir einen „Moment mal…“-Moment und stellten fest, dass die Lösung darin bestand, (in einigen Fällen) überhaupt nicht zu iterieren. Bei solchen Optimierungen kann man leicht den Wald vor lauter Bäumen nicht mehr sehen.
In anderen Fällen haben wir verschiedene Techniken verwendet, darunter:
- Heuristiken verwenden, um zu entscheiden, ob eine Optimierung keine lohnenden Ergebnisse liefert und daher übersprungen werden kann
- zusätzliche Datenstrukturen zum Zwischenspeichern berechneter Daten verwenden
- Änderung der aktuellen Datenstrukturen zur Leistungssteigerung
- Ergebnisse werden verzögert berechnet, um in einigen Fällen Zyklen zu vermeiden.
- Verwenden Sie die richtige Abstraktion. Unnötige Funktionen können den Code verlangsamen.
- Vermeiden Sie es, einen häufig verwendeten Zeiger durch viele Ladevorgänge zu verfolgen.
Woher wissen wir, ob sich die Optimierungen lohnen?
Das ist das Schöne daran: Sie müssen es nicht. Wenn Sie feststellen, dass ein Bereich viel Kompilierzeit in Anspruch nimmt, und Sie Entwicklungszeit darauf verwendet haben, ihn zu optimieren, finden Sie manchmal einfach keine Lösung. Vielleicht gibt es nichts zu tun, die Implementierung dauert zu lange, ein anderer Messwert verschlechtert sich dadurch erheblich oder die Codebasis wird komplexer. Für jede erfolgreiche Optimierung, die Sie in diesem Blogpost sehen, gibt es unzählige andere, die nicht umgesetzt wurden.
Wenn Sie sich in einer ähnlichen Situation befinden, versuchen Sie, den Messwert mit möglichst wenig Aufwand zu verbessern. Das bedeutet in der Reihenfolge:
- Schätzungen auf Grundlage von Messwerten, die Sie bereits erfasst haben, oder auf Grundlage eines Bauchgefühls
- Schätzung mit einem einfachen Prototyp
- Implementieren Sie eine Lösung.
Vergessen Sie nicht, die Nachteile Ihrer Lösung zu schätzen. Wenn Sie beispielsweise zusätzliche Datenstrukturen verwenden möchten, wie viel Arbeitsspeicher sind Sie bereit, dafür zu nutzen?
Detailliertere Informationen
Sehen wir uns einige der Änderungen an, die wir vorgenommen haben.
Wir haben eine Änderung zur Optimierung der Methode „FindReferenceInfoOf“ vorgenommen. Bei dieser Methode wurde ein Vektor linear durchsucht, um einen Eintrag zu finden. Wir haben die Datenstruktur so aktualisiert, dass sie nach der ID der Anleitung indexiert wird. Dadurch ist FindReferenceInfoOf O(1) statt O(n). Außerdem haben wir den Vektor vorab zugewiesen, um eine Größenanpassung zu vermeiden. Wir haben den Arbeitsspeicher leicht erhöht, da wir ein zusätzliches Feld hinzufügen mussten, das zählte, wie viele Einträge wir in den Vektor eingefügt haben. Das war jedoch ein geringes Opfer, da der maximale Arbeitsspeicher nicht gestiegen ist. Dadurch wurde die LoadStoreAnalysis-Phase um 34–66% beschleunigt, was zu einer Verbesserung der Kompilierungszeit um etwa 0,5–1,8% führt.
Wir haben eine benutzerdefinierte Implementierung von HashSet, die wir an mehreren Stellen verwenden. Das Erstellen dieser Datenstruktur hat viel Zeit in Anspruch genommen. Wir haben herausgefunden, warum. Vor vielen Jahren wurde diese Datenstruktur nur an wenigen Stellen verwendet, an denen sehr große HashSets zum Einsatz kamen. Sie wurde so angepasst, dass sie für diesen Zweck optimiert ist. Heutzutage wird sie jedoch in umgekehrter Richtung verwendet, mit nur wenigen Einträgen und einer kurzen Lebensdauer. Das bedeutete, dass wir Ressourcen verschwendeten, indem wir dieses riesige HashSet erstellten, es aber nur für einige Einträge verwendeten, bevor wir es wieder verwarfen. Mit dieser Änderung haben wir die Kompilierungszeit um etwa 1, 3 bis 2% verkürzt. Außerdem sank die Arbeitsspeichernutzung um etwa 0,5 bis 1 %, da wir nicht mehr so große Datenstrukturen verwendeten.
Wir haben die Kompilierungszeit um etwa 0,5 bis 1% verbessert, indem wir Datenstrukturen per Referenz an das Lambda übergeben haben, um das Kopieren zu vermeiden. Das wurde bei der ursprünglichen Überprüfung übersehen und war jahrelang in unserer Codebasis vorhanden. Erst durch die Analyse der Profile in pprof haben wir bemerkt, dass diese Methoden viele Datenstrukturen erstellen und wieder löschen. Deshalb haben wir sie genauer untersucht und optimiert.
Wir haben die Phase, in der die kompilierte Ausgabe geschrieben wird, durch Zwischenspeichern berechneter Werte beschleunigt. Das hat zu einer Verbesserung der gesamten Kompilierungszeit von etwa 1,3 bis 2,8% geführt. Leider war der zusätzliche Aufwand zu hoch und unsere automatisierten Tests haben uns auf die Speicherregression aufmerksam gemacht. Später haben wir uns den Code noch einmal angesehen und eine neue Version implementiert, die nicht nur die Speicherregression behoben, sondern auch die Kompilierungszeit um weitere 0,5–1,8 % verbessert hat. Bei dieser zweiten Änderung mussten wir die Funktionsweise dieser Phase überarbeiten und neu konzipieren, um eine der beiden Datenstrukturen zu entfernen.
Unser optimierender Compiler hat eine Phase, in der Funktionsaufrufe inline eingefügt werden, um die Leistung zu verbessern. Um auszuwählen, welche Methoden inline eingefügt werden sollen, verwenden wir sowohl Heuristiken, bevor wir Berechnungen durchführen, als auch endgültige Prüfungen, nachdem wir die Arbeit erledigt haben, aber kurz bevor wir das Inlining abschließen. Wenn eine dieser Heuristiken feststellt, dass sich das Inlining nicht lohnt (z. B. weil zu viele neue Anweisungen hinzugefügt würden), wird der Methodenaufruf nicht inline ausgeführt.
Wir haben zwei Prüfungen aus der Kategorie „Letzte Prüfungen“ in die Kategorie „Heuristisch“ verschoben, um zu schätzen, ob eine Inline-Einbindung erfolgreich sein wird oder nicht, bevor wir zeitaufwendige Berechnungen durchführen. Da es sich um eine Schätzung handelt, ist sie nicht perfekt.Wir haben jedoch überprüft, dass unsere neuen Heuristiken 99,9% der zuvor inline eingefügten Elemente abdecken, ohne die Leistung zu beeinträchtigen. Eine dieser neuen Heuristiken betraf die erforderlichen DEX-Register (Verbesserung von ca.0,2–1,3 %) und die andere die Anzahl der Anweisungen (Verbesserung von ca.2 %).
Wir haben eine benutzerdefinierte Implementierung eines BitVector, die wir an mehreren Stellen verwenden. Wir haben die BitVector-Klasse mit variabler Größe durch eine einfachere BitVectorView-Klasse für bestimmte Bitvektoren mit fester Größe ersetzt. Dadurch werden einige Umwege und Laufzeitbereichsprüfungen vermieden und die Erstellung der Bitvektorobjekte wird beschleunigt.
Außerdem wurde die Klasse „BitVectorView“ für den zugrunde liegenden Speichertyp templatisiert (anstatt wie beim alten „BitVector“ immer „uint32_t“ zu verwenden). Dadurch können einige Vorgänge, z. B. Union(), auf 64-Bit-Plattformen doppelt so viele Bits gleichzeitig verarbeiten. Die Stichproben der betroffenen Funktionen wurden beim Kompilieren des Android-Betriebssystems um insgesamt mehr als 1% reduziert. Dies geschah im Rahmen mehrerer Änderungen [1, 2, 3, 4, 5, 6].
Wenn wir alle Optimierungen im Detail besprechen würden, wären wir den ganzen Tag beschäftigt. Wenn Sie an weiteren Optimierungen interessiert sind, sehen Sie sich einige andere Änderungen an, die wir vorgenommen haben:
- Buchhaltung hinzufügen, um die Kompilierungszeiten um etwa 0,6–1,6 % zu verkürzen.
- Daten nach Bedarf berechnen, um Zyklen nach Möglichkeit zu vermeiden.
- Code refaktorieren, um die Vorberechnung zu überspringen, wenn sie nicht verwendet wird.
- Vermeiden Sie einige abhängige Ladeketten, wenn der Zuweiser problemlos an anderen Stellen abgerufen werden kann.
- Ein weiteres Beispiel für das Hinzufügen einer Prüfung, um unnötige Arbeit zu vermeiden.
- Häufige Verzweigungen nach Registertyp (Core/FP) im Register-Allocator vermeiden.
- Achten Sie darauf, dass einige Arrays zur Kompilierzeit initialisiert werden. Verlassen Sie sich nicht auf clang.
- Einige Loops bereinigen: Verwenden Sie Bereichsschleifen, die von Clang besser optimiert werden können, da die internen Zeiger des Containers aufgrund von Schleifennebeneffekten nicht neu geladen werden müssen. Vermeiden Sie es, die virtuelle Funktion „HInstruction::GetInputRecords()“ in der Schleife über das inline eingefügte „InputAt(.)“ für jede Eingabe aufzurufen.
- Accept()-Funktionen für das Besuchermuster vermeiden: Nutzen Sie eine Compiler-Optimierung.
Fazit
Wir haben die Kompilierungsgeschwindigkeit von ART deutlich verbessert, wodurch Android flüssiger und effizienter geworden ist. Außerdem hat sich die Akkulaufzeit verbessert und die Geräte werden weniger heiß. Durch die sorgfältige Ermittlung und Implementierung von Optimierungen haben wir gezeigt, dass erhebliche Verbesserungen der Kompilierungszeit möglich sind, ohne dass die Arbeitsspeichernutzung oder die Codequalität beeinträchtigt werden.
Wir haben Tools wie pprof verwendet, um Profile zu erstellen, und waren bereit, Iterationen durchzuführen und weniger erfolgversprechende Ansätze aufzugeben. Die gemeinsamen Bemühungen des ART-Teams haben nicht nur die Kompilierungszeit um einen bemerkenswerten Prozentsatz verkürzt, sondern auch die Grundlage für zukünftige Fortschritte geschaffen.
Alle diese Verbesserungen sind im Android-Update zum Jahresende 2025 und für Android 12 und höher über Mainline-Updates verfügbar. Wir hoffen, dass dieser detaillierte Einblick in unseren Optimierungsprozess wertvolle Informationen über die Komplexität und die Vorteile der Compilerentwicklung bietet.
-
ProduktneuheitenWir freuen uns, heute die stabile Version 1.1.0 der AndroidX Security State-Bibliothek und die Version 1.0.0 der Security State Provider-Bibliothek ankündigen zu können.
Maunik Shah, Alec Garcia, Joseph Yong • Lesezeit: 4 Minuten -
ProduktneuheitenHeute veröffentlichen wir die ersten Aufgaben mit langem Horizont (Long-Horizon Tasks, LHT). Das sind Aufgaben von großer Komplexität, für die ein Entwickler mehrere Tage oder sogar eine Woche benötigt. Außerdem führen wir die agentische Bewertung ein, beginnend mit Agents von entsprechenden Modellanbietern.
Matthew McCullough • Lesezeit: 3 Minuten -
ProduktneuheitenDas Debugging über WLAN auf Android ist jetzt schneller, zuverlässiger und einfacher einzurichten als je zuvor. Mit ADB Wi‑Fi 2.0 haben wir einen neuen Server-Stack und eine intelligentere Netzwerkverwaltung eingeführt, um direkt auf das Feedback von Entwicklern zu reagieren, die Lücken bei der Nutzerfreundlichkeit gemeldet haben.
Steven Jenkins, Sherif Eid, Fabien Sanglard • Lesezeit: 1 Minute
Lassen Sie sich Woche für Woche die neuesten Informationen zur Android-Entwicklung zusenden.