paint-brush
Software gedeiht, wenn man sie nicht vorher tötet: Vorzeitige Optimierung und eine Geschichte von Java GCvon@wasteofserver
553 Lesungen
553 Lesungen

Software gedeiht, wenn man sie nicht vorher tötet: Vorzeitige Optimierung und eine Geschichte von Java GC

von Frankie6m2024/04/06
Read on Terminal Reader

Zu lang; Lesen

Übertreiben Sie es nicht mit Optimierungen, lassen Sie die Sprache für Sie arbeiten. Geschichten erzählen. Java-Upgrades steigern die Leistung kostenlos. Benchmarking, immer.
featured image - Software gedeiht, wenn man sie nicht vorher tötet: Vorzeitige Optimierung und eine Geschichte von Java GC
Frankie HackerNoon profile picture
0-item

Ist eine LinkedList schneller? Soll ich `for each` durch einen `iterator` ersetzen? Soll diese `ArrayList` ein `Array` sein? Dieser Artikel entstand als Reaktion auf eine Optimierung, die so bösartig war, dass sie sich dauerhaft in mein Gedächtnis eingebrannt hat.


Bevor wir uns direkt mit Java und den Möglichkeiten zur Beseitigung von Störungen befassen, sei es durch den Garbage Collector oder durch Kontextwechsel, wollen wir zunächst einen Blick auf die Grundlagen des Codeschreibens für Ihr zukünftiges Ich werfen.


Vorzeitige Optimierung ist die Wurzel allen Übels.


Sie haben es schon einmal gehört: Vorzeitige Optimierung ist die Wurzel allen Übels. Naja, manchmal zumindest. Beim Schreiben von Software bin ich fest davon überzeugt, dass man:


  1. so beschreibend wie möglich ; Sie sollten versuchen, Absichten so zu erzählen, als würden Sie eine Geschichte schreiben.


  2. so optimal wie möglich ; das heißt, Sie sollten die Grundlagen der Sprache kennen und entsprechend anwenden.

So beschreibend wie möglich

Ihr Code sollte Ihre Absicht zum Ausdruck bringen und dies hängt maßgeblich von der Art und Weise ab, wie Sie Methoden und Variablen benennen.


 int[10] array1; // bad int[10] numItems; // better int[10] backPackItems; // great

Alleine anhand des Variablennamens lässt sich bereits auf die Funktionalität schließen.


Während numItems abstrakt ist, sagt backPackItems viel über das erwartete Verhalten aus.


Oder sagen wir, Sie haben diese Methode:


 List<Countries> visitedCountries() { if(noCountryVisitedYet) return new ArrayList<>(0); } // (...) return listOfVisitedCountries; }

Was den Code angeht, sieht das mehr oder weniger in Ordnung aus.


Können wir das besser machen? Das können wir auf jeden Fall!


 List<Countries> visitedCountries() { if(noCountryVisitedYet) return Collections.emptyList(); } // (...) return listOfVisitedCountries; }

Das Lesen von Collections.emptyList() ist viel aussagekräftiger als new ArrayList<>(0);


Stellen Sie sich vor, Sie lesen den obigen Code zum ersten Mal und stoßen auf die Schutzklausel , die überprüft, ob der Benutzer tatsächlich Länder besucht hat. Stellen Sie sich außerdem vor, dass dies in einer langen Klasse vergraben ist. Das Lesen von Collections.emptyList() ist definitiv aussagekräftiger als new ArrayList<>(0) . Sie stellen außerdem sicher, dass es unveränderlich ist und dass Clientcode es nicht ändern kann.

So optimal wie möglich

Kennen Sie Ihre Sprache und verwenden Sie sie entsprechend. Wenn Sie ein double benötigen, müssen Sie es nicht in ein Double Objekt einschließen. Dasselbe gilt für die Verwendung einer List , wenn Sie eigentlich nur ein Array benötigen.


Beachten Sie, dass Sie Zeichenfolgen mit StringBuilder oder StringBuffer verketten sollten, wenn Sie den Status zwischen Threads teilen:


 // don't do this String votesByCounty = ""; for (County county : counties) { votesByCounty += county.toString(); } // do this instead StringBuilder votesByCounty = new StringBuilder(); for (County county : counties) { votesByCounty.append(county.toString()); }


Erfahren Sie, wie Sie Ihre Datenbank indizieren. Planen Sie Engpässe voraus und führen Sie die Zwischenspeicherung entsprechend durch. Alle oben genannten Punkte sind Optimierungen. Sie sollten sich dieser Art von Optimierungen bewusst sein und sie als Erste umsetzen.

Wie tötet man es zuerst?

Ich werde nie einen Schreiberling vergessen, den ich vor ein paar Jahren gelesen habe. Ehrlich gesagt, der Autor ruderte schnell zurück, aber es zeigt, wie viel Böses aus guten Absichten entstehen kann.


 // do not do this, ever! int i = 0; while (i<10000000) { // business logic if (i % 3000 == 0) { //prevent long gc try { Thread.sleep(0); } catch (Ignored e) { } } }

Ein Garbage Collector-Hack aus der Hölle!


Im Originalartikel können Sie mehr darüber lesen, warum und wie der obige Code funktioniert. Auch wenn der Exploit auf jeden Fall interessant ist, handelt es sich hierbei um eines der Dinge, die Sie niemals tun sollten.


  • Funktioniert durch Nebeneffekte, Thread.sleep(0) hat in diesem Block keinen Zweck
  • Funktioniert durch Ausnutzen eines Code-Mangels im Downstream
  • Für jeden, der diesen Code erbt, ist er obskur und magisch


Beginnen Sie erst dann mit der Entwicklung komplexerer Texte, wenn Sie nach dem Schreiben mit allen von der Sprache bereitgestellten Standardoptimierungen auf einen Engpass gestoßen sind. Vermeiden Sie jedoch Mixturen wie die oben beschriebenen.


Eine Interpretation des zukünftigen Garbage Collector von Java, „vorgestellt“ von Microsoft Copilot


Wie bekämpft man den Garbage Collector?

Wenn der Garbage Collector nach all dem immer noch Widerstand leistet, können Sie Folgendes versuchen:


  • Wenn Ihr Dienst so latenzempfindlich ist, dass Sie keine GC zulassen können, führen Sie ihn mit „Epsilon GC“ aus und vermeiden Sie GC vollständig .
    -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC


    Dadurch wird Ihr Speicher natürlich vergrößert, bis Sie eine OOM-Ausnahme erhalten. Entweder handelt es sich also um ein kurzlebiges Szenario oder Ihr Programm ist so optimiert, dass keine Objekte erstellt werden


  • Wenn Ihr Dienst etwas latenzempfindlich ist, die zulässige Toleranz aber einen gewissen Spielraum lässt , führen Sie GC1 aus und geben Sie etwas wie -XX:MaxGCPauseTimeMillis=100 ein (Standard ist 250 ms).

  • Wenn das Problem von externen Bibliotheken herrührt , beispielsweise wenn eine davon System.gc() oder Runtime.getRuntime().gc() aufruft, bei denen es sich um Garbage Collector handelt, die die Welt stoppen, können Sie das fehlerhafte Verhalten durch Ausführen mit -XX:+DisableExplicitGC überschreiben.


  • Wenn Sie eine JVM über 11 verwenden, probieren Sie unbedingt den Z Garbage Collector (ZGC) aus. Die Leistungsverbesserungen sind enorm! -XX:+UnlockExperimentalVMOptions -XX:+UseZGC . Vielleicht möchten Sie sich auch diesen JDK 21 GC-Benchmark ansehen.


VERSION START

VERSION ENDE

STANDARD-GC

Java 1

Java 4

Serieller Garbage Collector

Java 5

Java 8

Paralleler Garbage Collector

Java 9

laufend

G1 Müllsammler


Hinweis 1: Seit Java 15 ist ZGC produktionsbereit , Sie müssen es jedoch noch explizit mit -XX:+UseZGC aktivieren.


Hinweis 2: Die VM betrachtet Maschinen als Server-Klasse, wenn die VM mehr als zwei Prozessoren und eine Heap-Größe größer oder gleich 1792 MB erkennt. Wenn es sich nicht um eine Server-Klasse handelt, wird standardmäßig der Serial GC verwendet .


Entscheiden Sie sich im Wesentlichen für GC-Tuning, wenn klar ist, dass die Leistungseinschränkungen der Anwendung direkt mit dem Garbage Collection-Verhalten zusammenhängen und Sie über das notwendige Fachwissen verfügen, um fundierte Anpassungen vorzunehmen. Andernfalls vertrauen Sie den Standardeinstellungen der JVM und konzentrieren Sie sich auf die Optimierung des Codes auf Anwendungsebene.

u/shiphe - du solltest den ganzen Kommentar lesen


Andere relevante Bibliotheken, die Sie möglicherweise erkunden möchten:

Java Microbenchmark Harness (JMH)

Wenn Sie ohne echte Benchmarks aus dem Gefühl heraus optimieren , tun Sie sich selbst keinen Gefallen. JMH ist die De-facto -Java-Bibliothek zum Testen der Leistung Ihrer Algorithmen. Verwenden Sie sie.

Java-Thread-Affinität

Das Fixieren eines Prozesses auf einen bestimmten Kern kann die Anzahl der Cache-Treffer verbessern. Dies hängt von der zugrunde liegenden Hardware und davon ab, wie Ihre Routine mit Daten umgeht. Diese Bibliothek macht die Implementierung jedoch so einfach, dass Sie eine CPU-intensive Methode testen sollten, wenn Sie Probleme mit ihr haben.

LMAX-Disruptor

Dies ist eine dieser Bibliotheken, die Sie studieren möchten, auch wenn Sie sie nicht brauchen. Die Idee besteht darin, Parallelität mit ultraniedriger Latenz zu ermöglichen. Aber die Art und Weise, wie sie implementiert wird, von mechanischer Sympathie bis zum Ringpuffer, bringt viele neue Konzepte mit sich. Ich erinnere mich noch daran, wie ich sie vor sieben Jahren zum ersten Mal entdeckte und eine ganze Nacht durchmachte, um sie zu verarbeiten.

Netflix jvmquake

Die Prämisse von jvmquake ist, dass Sie möchten, dass die JVM abstürzt und nicht hängen bleibt, wenn etwas schief geht. Vor ein paar Jahren habe ich Simulationen auf einem HTCondor-Cluster ausgeführt, der enge Speicherbeschränkungen hatte, und manchmal blieben Jobs aufgrund von „Nicht genügend Arbeitsspeicher“-Fehlern hängen.


Diese Bibliothek erzwingt einen Abbruch der JVM, sodass Sie sich mit dem eigentlichen Fehler befassen können. In diesem speziellen Fall würde HTCondor den Job automatisch neu planen.

Abschließende Gedanken

Der Code, der mich dazu gebracht hat, diesen Beitrag zu schreiben? Ich habe schon viel Schlimmeres geschrieben. Und das tue ich immer noch. Das Beste, worauf wir hoffen können, ist, dass wir kontinuierlich weniger Fehler machen.


Ich rechne damit, dass ich in einigen Jahren unzufrieden sein werde, wenn ich mir meinen eigenen Code anschaue.


Und das ist ein gutes Zeichen.



Änderungen und Dank:


Auch veröffentlicht auf wasteofserver.com