Java Performance - Dos and Don'ts?

Ich arbeite im Moment an einem Projekt zur Simulation von Wegfindungsberechnungen. Leider ist der Java-Part so langsam dass auch auf einem leistungsstarken Entwickler-Rechner die Performance zu wünschen übrig lässt.

Nach Auswertung mit dem Profiler bin ich genau so schlau wie vorher.

Mich würde interessieren: Was sind eure Dos and Don’ts wenn es um die Performance von Java-Programmen geht?

Wenn sich der Flaschenhals so nicht finden ließ bleibt in der Regel nur übrig, den Algorithmus grundlegend zu überdenken.
Durch simplere Datenstrukturen und die Verwendung von primitiven Datentypen kann man ggf. einiges rausholen. Derartige Flaschenhälse findet man in der Regel aber auch mit dem Profiler.

Das kann ja nicht sein.

Das erinnert mich an ein Problem, welches ich vor langer Zeit mal beim morphen von 3D-Objekten hatte. Keine Peformance auf irgend einem Rechner der Welt reichte für eine ruckelfreie Animation aus, bis ich darauf kam, dass ich in kürzester Zeit viel zu viele Objekte pro Animationsframe (in diesem Falle Vertices) neu erstellte, womit die JVM nicht hinterher kam. Die Lösung war am Ende sog. Object-Reuse, was die ständige Instanzierung reduziert. Object-Reuse funktioniert jedoch nur mit Mutables und zwar ungefähr so:

MutableObject obj = null;
for(int a=start; a<end; a++}
  obj=doSomething(obj);
  obj.copyInto(ObjectList[a]);
}

MutableObject doSomething(MutableObject target) {
  if(target==null || target.doesNotFit()} {
    target = new MutableObject();
  }
  fillWithNewData(target);
  return target;

Nun kenne ich deinen Code nicht, aber wenn im Profiler die Anzahl der Instanzierungen eines Datentyps etwa so hoch wie deren Übergaben an GC sind, ist die Lösung genau dies - Escape-Analysis hin oder her.

Die Frage ist vieeel zu allgemein. In der aktuellen Form öffnet sie Tür und Tor für irgendwelche anekdotischen Performance-Mysteries ("Mach’ Methodenparameter final" oder "Mach’ alle fields public" :nauseated_face: ).

Der Profilerdurchlauf sollte ja schonmal (zumindest grob) aufzeigen, wo die Rechenzeit verbrannt wird.

Wie hast du denn profilet? jVisualVM Profiler oder Sampler, oder mit dem HotSpot FlightRecorder…?

@All
Naja ein Profiler nennt mir die Ist-Werte, nicht die Soll-Werte. Wenn mir jemand sagt der Schrank passt nicht durch die Tür, dann hilft mir ein Meterstab auch nicht weiter, denn der sagt mir auch nur dass der Schrank nicht durch die Tür passt.

Woran erkenne ich denn potentielle Flaschenhälse im Profiler?

@anon19643277
Ja, das war auch eines der Probleme die ich behoben habe. Der GC kam nicht mit dem Aufräumen hinterher, was sehr schön an der RAM-GC-Kurve ersichtlich war. Nach wenigen Sekunden war der Speicher voll und dann hat der GC wohl sowas wie einen Deepscan gemacht was das Programm zum anhalten brachte.

@Marco13
Das weiß ich nicht. Wir haben ein vorinstalliertes IntelliJ Plugin. Es bietet alles was man braucht und die Oberfläche ist relativ selbsterklärend, deshalb musste ich nie fragen.

Bei einem Algorithmus kann man nur dort einsparen, wo man auch tatsächlich was verbraucht.

Ein Profiler zeigt in erster Linie, an welchen Stellen es einen Verbrauch gibt und wie hoch dieser ist. Dann schaut man sich an wo der höchste Verbrauch ist und überlegt ob sich dort Optimierungspotential findet.
Was nützt eine Optimierung um 90% einer Methode, wenn diese nur einmalig aufgerufen wird und 10 ms benötigt?
In manchen Algorithmen findet sich auch sowas, dass eine Funktion mit den selben Parametern mehrmals aufgerufen wird. Da kann man zum Beispiel die Ergebnisse Cachen. Fibonacci-Rekursiv ist zum Beispiel so ein Fall an dem das deutlich wird.

Das mit dem vorinstallierten Plugin, ist ja schon die Krux der Geschichte. Einfach zu bedienen, spuckt Werte aus. Die Werte helfen aber nicht weiter um das Problem zu lösen, geschweige denn ob es auch nützliche Werte sind.

Aber Schrank und Tür sind ja schon mal ein gutes Beispiel. Was muss man denn nun Messen um zu entscheiden ob ein Schrank durch eine Tür geht? Beim Schrank sind es die kleinsten Flächen. Passt dies nicht, dann baut man den Schrank auseinander und misst dort die kleinsten Flächen. Man kann auch vor dem Auseinanderbauen messen. Und dann kann man irgendwann entscheiden ob der Schrank tatsächlich nicht durch die Tür passt.

alternativ bitte folgendes zu Bedenken: Manchmal ist die Laufzeit anhand des Codes zu erkennen, dann wird ein Profiler nicht unbedingt gebraucht. Wenn die Laufzeit zB linear oder quadratisch ist, aber eigentlich konstant sein könnte. (nur als Hinweis)

Ansonsten, zu den Dos and Don’ts gibt es ein tolles Buch, Effektiv/wirkungsvoll Java von Joshua Bloch komplett Englisch, und schon von 2001. In dem Buch beschriebt jemand, der prinzipiell von Anfang dabei war, was zu tun/ was zu lassen.

Meiner Meinung nach und meines Wissen nach, hat es an Aktualität nix verloren!

Sicherlich mag es auch andere gute Bücher zu dem Thema geben…

“Effective Java” von Josh Bloch ist zwar praktisch uneingeschränkt empfehlenswert, hat aber mit Performance nicht viel zu tun.

@TMII Wenn man nicht mal weiß, welchen Profiler man verwendet, wird es schwierig. Aber da ich davon ausgehe, dass du wüßtest, wenn du einen 500-Dollar-YourKit da werkeln hättest, gehe ich davon aus, dass das nur die Arme-Leute-Lösung ist, d.h. VisualVM

Da ist es ja so, dass man seine Anwendung startet, dann das “Sampler”-Tab auswählt, auf “CPU” klickt, und dann eine Weile wartet… das sieht dann ja etwa so aus:

(Von https://blogs.oracle.com/nbprofiler/visualvm-13-released )

Dann wartet man entweder, bis die Anwendung beendet ist, oder man klickt zwischendrin auf “Snapshot”. In der “Snapshot”-Ansicht gibt es unten ein praktisches Tab namens “Combined”, wo man zwei verlinkte Views hat: Man kann sich entweder durch die Baumansicht nach unten browsen,und sieht dann, in welchen Methoden wie viel Zeit verbrannt wird, oder man klickt unten auf “Hotspots”, um in der Baumansicht gleich den passenden Pfad auszuwählen.

Da sollte man doch schon ein paar Einsichten rausziehen können…?!

1 „Gefällt mir“

Don’t optimise without a profiler
Do measure the impact of your changes with a profiler

:slight_smile:

1 „Gefällt mir“

@maki
So ziemlich die hilfreichste Antwort bisweilen.

----------- Closed ----------

War das jetzt ironisch? Ziemlich enttäuschendes Ende für so einen Thread :confused:

Na, mal im Ernst, @TMII, ich meine mich zu erinnern, dass du das eine oder andere mal die eine oder andere nicht ganz uninteressante Sache gemacht hast, und wie und wo du da jetzt weitergemacht hast, könnte interessant sein…

Mein Ironie Sensor funzt nicht immer ganz, nur fuer den Fall das mein Beitrag als Ironie aufgefasst wurde:
War nicht so gemeint, sondern schlicht als Zusammenfassung was andere schon erwaehnt haben,
wenn man keinen Weg hat festzustellen wo die Zeit verbraten wird, fuehrt das nur zum rumprobieren, wenig aussichtsreich.

Selbst wenn man meint man hat besseren Code der viel schneller ist, muss man das nachweisen koennen, sonst hat man im Endeffekt nur den Code komplizierter gemacht, oft sogar langsamer IME.

Da gibt es die 80/20 Regel, die besagt dass 20% vom Code 80% der Ressourcen “verbraten”, man kann viel rumdoktorn an den anderen 80% des Codes, ohne dass es zu einer messbaren Verbesserung fuehrt.

Das ist der Grund warum Profiler wichtig sind:
Sie zeigen uns wo die Ressourcen verbraten werden, und ob unsere Aenderungen das verbessern.

Die Frage nach der Ironie bezog sich auf TMIIs Antwort - eben weil der Hinweis von dir eher nach etwas klang, was man als “Binsenweisheit” bezeichnen würde. Die Frage zielte ja (anscheinend, und sofern ich das nicht vollkommen falsch verstanden habe) gerade darauf ab, wie man mit einem Profiler hier gezielt die relevante Information rausziehen kann.

(Da gibt es einige interessante Caveats. Um nur eine zu nennen, die ich gerade für VisualVM für recht wichtig halte: Je nachdem, ob man die Analyse mit dem “Sampler” oder dem “Profiler” macht, können völlig unterschiedliche Ergebnisse rauskommen. Wer von beiden “eher die Wahrheit sagt”, oder wie man die Ergebnisse zusammenführt, um sinnvolle Schlüsse daraus zu ziehen, ist eine interessante Frage, auf die ich bisher keine wirklich zufriedenstellende Antwort habe)

Ich hatte die Frage ursprünglich tatsächlich anders verstanden. Der Eröffnungspost las sich für mich so, als wenn TMII mit dem Profiler (unter Anwendung der „gängigen“ Techniken) kein Optimierungspotenzial festgestellt hat. Daher kam auch mein Einwand, dass man es dann mit einem grundlegend anderen Ansatz versuchen müsse.

Dass es um die Anwendung eines Profilers geht, hat TMII meines Erachtens erst in seinem zweiten Beitrag deutlich gemacht.

Ich mich auch :slight_smile:
War mir nicht sicher ob @TMII Antwort auf meine binsenweisheit ironisch gemeint war.

Nicht ironisch und Sarkasmus umschreibt es auch nicht ganz korrekt. Eher Satire.
Ich hab viel zu tun, wenig Schlaf und muss mich im Moment neu formieren und meine Fragestellung überdenken und präzisieren.

Vielleicht zeige ich mal ein Stelle die mir suspekt vorkommt:
Der Profiler zeigt mir eine Methode an in der relativ viel Zeit verbraten wird (2%), sie wird im Beispielprogramm auch überproportional aufgerufen (mehrere tausend mal pro Sekunde) und steigt proportional mit der Simulation.

public boolean isComplexType() {
return this.mobileType == MobileType.COMPLEX || this.positionValue <= 0 && this.posXQuantifier < EPSILON;
}

Das Ergebnis ist nicht immer das gleiche, sowohl Objektübergreifend als auch auf einzelne Objekte bezogen.
Kann ich trotzdem nicht verstehen, daß solche elementaren Operationen so viel Zeit verbraten. Vielleicht liegt es aber auch am Profiler der eine performierung durch die JVM verhindert,

2% sieht jetzt nicht nach wirklich viel aus. Selbst wenn das komplett wegoptimiert wird, wird das Programm nur um 2% schneller.
Da ist nicht wirklich ein Blumentopf zu gewinnen.

Die Frage die man nun beantworten muss ist, muss diese Methode so oft aufgerufen werden?

Wie oft wird die Methode auf dem gleichen Objekt aufgerufen?

Wie ändern sich die Fields moblieType, positionValue, posXQuantifier, EPSILON während der Laufzeit?
Sind diese Fields final?

Wie sehen die Datentypen aus? Muss da eventuell gecastet werden?

Auch wenn diese Methode recht einfach aussieht, müssen doch irgendwo her die Werte dieser Felder aus dem Speicher geholt werden. Da kann schon ein bisschen was zusammenkommen.

Im Idealfall ist alles final und man legt sich ein boolean complexType an, dass beim initialisieren gesetzt wird. Dann muss am Ende auch nur dieses eine Boolean abgefragt werden.
Ändern sich die Werte nur selten, kann man sich auch überlegen ob man einen “Listener” auf Veränderungen lauschen lässt, der dann ComplexType ein Update verpasst. Also beim setter von positionValue nochmals die Berechnung aufruft die den Boolean von ComplexType setzt.

Zudem kann man auch die einzelnen Geschichten Benchmarken um zu sehen, ob da wirklich relevante Werte/Verbesserungen zusammenkommen.

War das jetzt mit dem (VisualVM) Sampler oder Profiler? (Leider bewirkt letzterer ja unter anderem, dass das Program laaanngsaaam wird, und ist deswegen nur schwer auf “real world”-Läufe anwendbar…)

ionutbaiu hat schon die relevantesten Punkte genannt. Wie sieht’s denn aus, wenn man in der Aufrufhierarchie hochgeht? Ich meine, die VisualVM bricht die Zeiten (pro Thread) ja schon schön runter. Wenn nun 2% der größte Block ist, und ansonsten alles im 1.x%-Bereich “verschmiert über die ganze Codebasis” liegt, wird’s schwierig, aber … vielleicht lassen sich ja “einfache” Muster erkennen.