Java-Code-Optimierung in Android Marshmallow

Ursprünglicher Autor: Rahul K.
  • Übersetzung
Verbesserung der Systemleistung, Verbesserung des Nutzererlebnisses mit Anwendungen: Dies sind die Bereiche, in denen sich Android entwickelt. Android Marshmallow bietet viele neue Funktionen und Möglichkeiten. Insbesondere sprechen wir über wesentliche Verbesserungen der Android Runtime (ART). Sie konzentrieren sich auf Leistung, Speicherverbrauch und Multitasking.

Neues Plattform-Release veröffentlicht? Die virtuelle Android-Maschine hat sich geändert? Jedes dieser Ereignisse bedeutet, dass der Entwickler die Essenz der Innovationen dringend verstehen muss. Wir müssen nämlich herausfinden, welche Methoden, mit denen wir in der Vergangenheit Hochleistungslösungen erzielen konnten, jetzt nicht mehr so ​​effektiv sind. Wir müssen neue Ansätze für die Anwendungsentwicklung finden, mit denen sich die besten Ergebnisse erzielen lassen. Über solche Feinheiten ist fast nichts geschrieben, daher müssen Entwickler alles durch Ausprobieren herausfinden.

Heute werden wir darüber sprechen, wie man schnelle und bequeme Programme unter Berücksichtigung der Funktionen der aktualisierten Android Runtime schreibt. Unsere Tipps zielen darauf ab, die Produktivität zu steigern und die Qualität des Maschinencodes zu verbessern, der aus Java-Texten generiert wird. Wir werden auch über die Features der Low-Level-Optimierung sprechen, die nicht immer vom Java-Quellcode der Anwendung abhängen.

Java-Code, Leistungsverbesserung und ART


Android ist ein System mit einer recht komplizierten internen Struktur. Eines seiner Elemente ist ein Compiler, der Java-Code in Maschinenanweisungen konvertiert, beispielsweise auf Geräten, die auf Intel-Prozessoren basieren. Android Marshmallow enthält einen Optimizing-Compiler. Dieser neue Compiler optimiert Java-Anwendungen und erstellt Code, der produktiver ist als der vom Quick-Compiler in Android Lollipop erstellte.

In Marshmallow werden fast alle Anwendungen mithilfe eines optimierenden Compilers für die Ausführung vorbereitet. Android System Framework-Methoden verwenden jedoch weiterhin den schnellen Compiler. Dies geschieht, um Android-Entwicklern mehr Debugging-Möglichkeiten zu bieten.

Genauigkeits-, Geschwindigkeits- und Mathematikbibliotheken


Um Gleitkommaberechnungen zu organisieren, können Sie verschiedene Implementierungen derselben Operationen verwenden. In Java sind also die Bibliotheken Math und StrictMath verfügbar. Sie bieten unterschiedliche Genauigkeitsstufen bei der Ausführung von Gleitkommaberechnungen. Und obwohl Sie mit StrictMath besser vorhersehbare Ergebnisse erzielen können, ist die Mathematikbibliothek in den meisten Fällen ausreichend. Hier ist eine Methode, mit der der Kosinus berechnet wird.

public float myFunc (float x) {
    float a = (float) StrictMath.cos(x); 
    return a;
}

Hier haben wir die entsprechende Methode aus der StrictMath-Bibliothek verwendet, aber wenn der Verlust an Berechnungsgenauigkeit für ein bestimmtes Projekt akzeptabel ist, ist es in einer ähnlichen Situation durchaus möglich, Math.cos (x) zu verwenden. Bei der Auswahl einer geeigneten Bibliothek ist zu berücksichtigen, dass die Math-Klasse für die Verwendung der Android Bionic-Bibliothek für Intel-Architektur optimiert ist. Infolgedessen werden Operationen von Math 3,5-mal schneller ausgeführt als ähnliche Operationen von StrictMath.

In manchen Situationen ist StrictMath natürlich unverzichtbar, und es ist unerwünscht, es durch Math zu ersetzen. In den meisten Fällen ist es jedoch durchaus möglich, Math zu verwenden. Die Auswahl einer bestimmten Bibliothek ist keine abstrakte Suche nach einem Kompromiss zwischen Geschwindigkeit und Genauigkeit. Hier sollten Sie die Projektanforderungen so vollständig wie möglich berücksichtigen - von den Funktionen des Algorithmus und seiner Implementierung bis hin zur Hardwareplattform, auf der Sie die Anwendung verwenden möchten.

Unterstützt rekursive Algorithmen


Rekursive Aufrufe in Marshmallow sind effizienter als in Lollipop. Wenn rekursiver Code für Lollipop geschrieben wird, werden immer Daten aus dem DEX-Cache geladen. In Marshmallow wird dasselbe aus der ursprünglichen Argumentliste übernommen und nicht erneut aus dem Cache geladen. Natürlich ist der Leistungsunterschied zwischen Lollipop und Marshmallow umso größer, je größer die Tiefe der Rekursion ist. Wenn ein rekursiv implementierter Algorithmus jedoch iterativ umgeschrieben werden kann, ist seine Leistung in Marshmallow höher.

Keine Überprüfung auf Auslandsaufenthalte für ein Array: array.length


Mit dem optimierenden Compiler in Marshmallow kann in einigen Fällen vermieden werden, dass nach einem ausgehenden Array gesucht wird (Bound Check Elimination, BCE).

Schauen Sie sich die leere Schleife an:

for (int i = 0; i < max; i++) { }

Die Variable i wird als Induktionsvariable (Induktionsvariable, IV). Wenn eine solche Variable verwendet wird, um den Zugriff auf das Array zu organisieren, und die Schleife alle ihre Elemente durchläuft, können Sie nicht überprüfen, ob die Variable max explizit auf den Wert gesetzt ist, der der Länge des Arrays entspricht.

Stellen Sie sich ein Beispiel vor, in dem der Code die Variablengröße verwendet, die die Rolle des Maximalwerts für eine induktive Variable spielt.

int sum = 0;
for (int i = 0; i < size; i++) {
    sum += array[i];
}

Im Beispiel wird der Index des Elements des Arrays i mit der Variablengröße verglichen. Angenommen, die Größe wird entweder außerhalb der Methode festgelegt und als Argument übergeben oder irgendwo innerhalb der Methode definiert. In diesen Fällen kann der Compiler nicht feststellen, ob der Wert in der Variablengröße mit der Länge des Arrays übereinstimmt. Infolge dieser Unsicherheit muss der Compiler einen Code generieren, um zu überprüfen, ob der i-Wert die Grenzen des Arrays überschreitet. Dieser gelangt in die ausführbare Datei und wird bei jedem Zugriff auf das Array ausgeführt.

Wir erstellen die Schleife neu, damit der Compiler die Prüfung auf Überschreitung der Grenzen des Arrays im ausführbaren Code aufhebt.

int sum = 0;
for (int i = 0; i < array.length; i++) {
    sum += array[i];
}

Beseitigen Sie die Prüfung auf Überschreitung der Grenzwerte für mehrere Arrays


Im vorherigen Abschnitt haben wir den einfachsten Optimierungsfall untersucht und untersucht, was zu tun ist, damit der Compiler Code generiert, in dem keine Prüfungen für das Überschreiten der Grenzen des Arrays vorhanden sind. Es gibt jedoch Algorithmen, bei denen mehrere verschiedene Arrays mit derselben Länge in demselben Zyklus verarbeitet werden. Wenn Sie beispielsweise zwei Arrays in einer Schleife verarbeiten, muss der Compiler prüfen, ob beide Arrays null sind und außerhalb der Grenzen liegen.

Betrachten wir die Optimierungstechnik für die Verarbeitung mehrerer Arrays in einem Zyklus genauer. Häufig müssen Sie den Code neu schreiben, damit der Compiler die Schleife optimieren kann. Hier ist die Originalversion.

for (int i = 0; i < age.length ; i++) {
    totalAge += age[i];
    totalSalary += salary[i];
}

Darin liegt ein Problem. Das Programm überprüft die Länge des Gehalt-Arrays überhaupt nicht, daher besteht die Gefahr, dass eine Ausnahme auftritt, die über die Grenzen des Arrays hinausgeht. Eine solche Kontrolle sollte beispielsweise am Eingang des Fahrrads erfolgen.

for (int i = 0; i < age.length && i < salary.length; i++) {
    totalAge += age[i];
    totalSalary += salary[i];
}

Das Java-Programm sieht jetzt viel besser aus, aber mit diesem Ansatz wird die Prüfung, ob die Grenzen von Arrays überschritten werden, in den Maschinencode übernommen.
In der obigen Schleife arbeitet der Programmierer mit zwei eindimensionalen Arrays: Alter und Gehalt. Und obwohl die induktive Variable durch Vergleich mit der Länge von zwei Arrays getestet wird, ist die Bedingung mehrfach und der Compiler kann den außerhalb der Grenzen von Arrays liegenden Code nicht aus dem ausführbaren Code ausschließen.

Arrays, auf die wir in Schleifen verweisen, sind Datenstrukturen, die in verschiedenen Speicherbereichen gespeichert sind. Infolgedessen sind die logischen Operationen, die an ihren Eigenschaften ausgeführt werden, unabhängig. Wir schreiben den Code neu, indem wir einen Zyklus in zwei teilen.

for (int i = 0; i < age.length; i++) {
    totalAge += age[i];
}          
for (int i = 0;  < salary.length;  i++) {
    totalSalary += salary[i];
}

Nach dem Trennen der Schleifen schließt der optimierende Compiler eine Prüfung für das Auslandsreisen sowohl für die Altersgruppe als auch für die Gehaltsgruppe aus dem ausführbaren Code aus. Infolgedessen kann Java-Code drei- bis viermal beschleunigt werden.

Die Optimierung war erfolgreich, aber jetzt enthält die Funktion zwei Zyklen und nicht einen. Ein solcher Ansatz kann zu einer ungerechtfertigten Vergrößerung der Ausgangstexte führen. Abhängig von der Zielarchitektur und der Größe der Schleifen oder der Anzahl solcher Optimierungen kann sich dies auf die Größe des generierten ausführbaren Codes auswirken.

Multithread-Programmiertechniken


In einem Multithread-Programm muss beim Arbeiten mit Datenstrukturen vorsichtig vorgegangen werden.

Angenommen, ein Programm startet vier identische Threads, bevor die unten gezeigte Schleife aufgerufen wird. Jeder der Threads arbeitet dann mit einem Array von aufgerufenen Ganzzahlen thread_array_sum. Jeder Stream ändert eine der Zellen des Arrays, dessen Adresse, die eine eindeutige Ganzzahl des Streams ist, in einer Variablen angegeben wird myThreadIdx.

for (int i = 0; i < number_tasks; i++) {
    thread_array_sum[myThreadIdx] += doWork(i);
}

Der für alle Prozessorkerne gemeinsame Cache der letzten Ebene (Last Level Cache, LLC) wird in einigen Hardwarearchitekturen, z. B. in der Intel Atom x5-Z8000-Prozessorreihe, nicht verwendet. Separate LLC ist eine mögliche Verringerung der Antwortzeit, da für jeden Prozessorkern (oder zwei Kerne) ein eigener Cache "reserviert" ist. Die Cache-Konsistenz muss jedoch beibehalten werden. Wenn ein auf Kernel A laufender Thread Daten ändert, die ein auf Kernel B laufender Thread niemals ändern wird, müssen daher die entsprechenden Zeilen im Cache von Kernel B aktualisiert werden . Dies kann zu Problemen mit der Leistung und der Kernel-Skalierung führen.

Aufgrund der Architektur des Caches besteht die Gefahr, dass mehrere Threads Daten in ein Array schreiben, da möglicherweise ein hohes Maß an Cache-Slippage auftritt. In einer solchen Situation sollte der Programmierer eine lokale Variable verwenden, um die Ergebnisse von Zwischenberechnungen zu speichern, und nach deren Abschluss das Ergebnis in ein Array schreiben. Mit diesem Ansatz kann der obige Zyklus konvertiert werden:

int tmp = 0;
    for (int i = 0; i < number_tasks; i++) {
    tmp += doWork(i);
}
thread_array_sum[myThreadIdx] += tmp;

In diesem Fall ist das Array-Element thread_array_sum[myThreadIdx]in der Schleife nicht betroffen. Der durch die Funktion erhaltene Endwert wird doWork()im Array-Element außerhalb der Schleife gespeichert. Dadurch wird das potenzielle Risiko eines Ausrutschens des Caches erheblich verringert. Ein Verrutschen kann bei einem einzelnen Zugriff auf das in der letzten Codezeile angezeigte Array auftreten, dies ist jedoch weniger wahrscheinlich.

Es wird nicht empfohlen, gemeinsam genutzte Datenstrukturen in Schleifen zu verwenden, es sei denn, die darin gespeicherten Werte sollten nach Abschluss jeder Iteration für andere Threads zugänglich sein. In solchen Fällen müssen Sie in der Regel mindestens die Felder oder Variablen verwenden, die mit dem Schlüsselwort volatile deklariert wurden, aber darüber zu sprechen, würde den Rahmen dieses Artikels sprengen.

Optimierung für Geräte mit kleinem Speicher


In Bezug auf RAM und permanenten Speicher gibt es sehr unterschiedliche Android-Geräte. Java-Programme sollten so geschrieben werden, dass sie unabhängig von der Speichergröße optimiert werden können. Wenn das Gerät über wenig Arbeitsspeicher verfügt, ist einer der Optimierungsfaktoren die Größe des Codes. Dies ist einer der Parameter von ART.

In Marshmallow werden Methoden, die größer als 256 Byte sind, nicht vorkompiliert, um permanenten Speicher auf dem Gerät zu speichern. Daher werden Java-Anwendungen, die häufig verwendete Methoden großer Größe enthalten, auf dem Interpreter ausgeführt, was sich negativ auf die Leistung auswirkt. Implementieren Sie häufig verwendete Code-Snippets als kleine Methoden, damit der Compiler sie vollständig optimieren kann, um eine höhere Anwendungsleistung in Android Marshmallow zu erzielen.

Schlussfolgerungen


Jede Veröffentlichung von Android ist nicht nur ein neuer Name, sondern auch neue Elemente des Systems und der Technologie. So war es mit KitKat und Lollipop, dies gilt auch für den Übergang zu Marshmallow, der bedeutende Änderungen bei der Zusammenstellung von Anwendungen mit sich brachte.

Wie bei Lollipop verwendet ART die Ahead-of-Time-Kompilierungsmethode, wobei Anwendungen während der Installation normalerweise in systemeigenen Code konvertiert werden. Anstatt jedoch den Lollipop Quick Compiler zu verwenden, verwendet Marshmallow den neuen Optimizing-Compiler. Obwohl in einigen Fällen die Übertragung des optimierenden Compilers auf den alten funktioniert, ist der neue Compiler das Hauptsubsystem für die Erstellung von Binärcode aus Java-Texten unter Android.

Jeder Compiler hat seine eigenen Funktionen und Optimierungswerkzeuge, sodass jeder von ihnen je nach der Art und Weise, in der die Java-Anwendung geschrieben ist, unterschiedlichen Maschinencode generieren kann. In diesem Artikel haben wir über einige wichtige Features gesprochen, die in Marshmallow zu finden sind, und darüber, was Entwickler, die Anwendungen für die neue Plattform erstellen, wissen sollten.

Es gibt viele Neuerungen im optimierenden Compiler. Einige von ihnen lassen sich nur schwer anhand von Beispielen zeigen, da die meisten Verbesserungen sozusagen „unter der Haube“ erfolgen. Was wir wissen ist, dass mit dem zunehmenden Alter des Android-Compilers zu beobachten ist, wie die zugrunde liegenden Technologien immer perfekter werden und allmählich mit anderen optimierenden Compilern Schritt halten.

Der Android-Compiler wird ständig weiterentwickelt. Entwickler können sicher sein, dass der von ihnen geschriebene Code gut optimiert wird. Ihre Anwendungen werden die Benutzer mit der Geschwindigkeit und dem angenehmen Erlebnis der Interaktion mit ihnen begeistern. Wir sind sicher, dass diejenigen, die mit solchen Programmen arbeiten, es zu schätzen wissen.

Jetzt auch beliebt: