Java - Performancemythen

Kolumne
Aus der Java-Trickkiste: Performancemythen

Dazu kommt noch der Mythos, das Programmierung mit den neuen Features wie Lambdas durch zusätzliche Objekterzeugung langsam wäre. Zum einen wird bei vielen Aufrufen gar kein Objekt erzeugt (im Bytecode wird dann invokedynamic verwendet), zum anderen kann die VM auch Objekterzeugung durch Escape-Analyse vermeiden (siehe z.B. https://docs.oracle.com/javase/8/docs/technotes/guides/vm/performance-enhancements-7.html ).

Naja, es ist schon ein Punkt woran sie arbeiten müssen, v.a. bzgl. des Garbage der entsteht. Dazu kann man aus den Beiträgen von „mechanical sympathy“ was rausholen bzw. von Martin Thompson.

Mich wuerden vor allem echte Dinge interessieren, auf die man schon beim Programmieren achten sollte. Ich weiss schon, root evil und so - aber ich finde, man sollte den Performance-/Resourcengedanken zumindest nicht voellig aus den Augen verlieren. Klar, bei 99% der “Enterprise”-Funktionalitaeten isses voellstaendig egal, ob das nun 5 oder 10ms dauert - aber beim 60fps-Ziel is das was anderes.

Ist Auto-Boxing eigentlich immer noch teuer? Oder try-catch?

Vor einiger Zeit hatte ich mal echt Probleme mit irgendeiner Vector Klasse und mehr oder weniger komplexen Berechnungen in einem Render-Loop. Umstellen auf float x,z,y hat das dann spuerbar verbessert (deutlich weniger GC Last).

Autoboxing und unboxing können noch teuer sein. Erstens, weil es sehr schlecht für den Cache ist (als Extremfall etwa ein großer Integer[] array vs. ein int[] array). Zweitens, was ggf. noch viel wichtiger ist: Auch wegen der Garbage. Ich hatte mal irgendwann eine Berechnung, die zeitkritisch war und in einer Schleife lief (Training einer Self-Organizing Map). Da habe ich dann einige Funktionen, weil es sich anbot, als Function reingegeben, in verschiedenen Ausprägungen, aber immer kam ein Double drin vor. Dann habe ich das Training laufen lassen, und mich gewundert, warum es zwischendrin immer mal mehrere Sekunden lang (!!!) stehen blieb. Ein „-verbose:gc“ brachte die erste Einsicht: „Full GC, 3 Sekunden“. Eine genauere Analyse zeigte dann: Er war da schlicht damit beschäftigt, Millionen und Abermillionen von Double-Objekten wegzuschaufeln (dass ein nicht unerheblicher Teil davon 0.0en waren, ist eher eine unterhaltsame Randnotiz :rolleyes: ).

String-Konkatenation ist so eine Sache, wie in dem Artikel ja auch beschrieben ist. „final“ bringt praktisch nie was (in bezug auf Performance - bei Fields sollte „final“ verwendet werden wannimmer es möglich ist).

Was er da zu Vererbung sagt ist ein bißchen diffizil. Ich hatte ihn da auch mal nach einem JAX-Vortrag darauf hingewiesen: Es gibt ein paar mehr Freiheitsgrade, die es zu berücksichtigen gilt. Auch wenn „eine Antwort“ schwierig ist, so hilft sowas wie The Black Magic of (Java) Method Dispatch zumindest, zu erkennen, WARUM eine Antwort schwierig ist :wink:

Exceptions bzw. try-catch kommen bei einem „60 FPS-Spiel“ ja praktisch nicht vor, zumindest nicht im zeitkritischen Teil - deswegen sehe ich das als nicht sooo relevant an. Es soll ja tatsächlich Leute geben, die statt einer normalen for-Schleife sowas schreiben wie

try
{
    int index = 0;
    while (true)
    {
        process(array[index++]);
    }
}
catch (ArrayIndexOutOfBoundsException e)
{
    // Finished the array
}

in der Hoffnung, dass das durch die gesparte if-Abfrage schneller ist :rolleyes:

Das kritischste und unberechenbarste sind für mich auch immernoch die Allokationen. Mein weiß einfach nicht, wo genau die Escape Analysis tatsächlich greift. Jedenfalls habe ich gerade bei solchen vermeintlich trivialen und leicht zu analysierden Klassen wie „Vector3f“ und „Tuple2i“ schon häufiger gesehen, dass unbedachte Allokationen den GC ins Schleudern bringen. Wenn es wirklich um Performance geht, dann würde ich persönlich (und ich akzeptiere, wenn man da widerspricht) aus meiner bisherigen Erfahrung sagen, dass zumindest die Möglichkeit bestehen sollte, Allokationen zu vermeiden. Sowas wie

class Tuple2i {
    int x, y;

    Tuple2i add(Tuple2i other) {
        return new Tuple2i(x+other.x, y+other.y);
    }
}

würde ich also eher als

class Tuple2i {
    int x, y;

    Tuple2i add(Tuple2i other, Tuple2i result) {
        if (result==null) result = new Tuple2i();
        result.x = x+other.x;
        result.y = y+other.y;
        return result;
    }
}

schreiben - oder ähnlich, auf jeden Fall aber die Allokationen optional dem Aufrufer überlassen. Sowas wie GitHub - JOML-CI/JOML: A Java math library for OpenGL rendering calculations setzt das ähnlich um (ich muss mir das mal näher ansehen - vielleicht ersetze ich die etwas in die Jahre gekommene Vecmath in GitHub - javagl/Rendering: A rendering library damit…)

Ich wusste gar nicht, das manche Leute denken, final würde die Performance verbessern.

Ich konnte mich in dem Artikel nicht wiederentdecken. Eventuell sind diese “Mythen” wirklich nur bei Neulingen vorhanden - oder sie sind an mir komplett vorbei gegangen.

Der wichtigste Grundsatz in Sachen Performanz heißt: am schnellsten tut man das, was man nicht tut!
Und ich denke gerade da ist die Java9 Streams-API wenig hilfreich.

Beispiel?
Ich will eine Methode auf Objekten einer Collection aufrufen:myCollection.forech(o->o.myFancyMethod());
Was wird denn der Durchschnittsprogrammierer tun, wenn nun eine zweite Methode Aufzurufen ist?
Meine Vermutung ist, dass dann überduchschnittlich oft das raus kommt:myCollection.forech(o->o.myFancyMethod()); myCollection.forech(o->o.myOtherFancyMethod());Und schon haben wir eine unnötige zweite Iteration über die Liste…

[edit]
Verzweigungen in Schleifen sind genau so interessant:StringBuilder resultString = new StringBuilder(); for(MyType o :myCollection){ if(0<resultString.size()) resultString.append(" and "); resultString.append(o.toString()); } Ich will mich nicht darauf verlassen, das die Hotspot-JVM dass vernünftig in Binärcode übersetzt, so dass der “True-Pfad” der jenige ist, der in der Instuction-Pipeline des Prozessors liegt und die Pipeline nur bei der ersten Iteration verworfen und neu befüllt werden muss.

Deshalb sehen meine String-Konkatenierungsschleifen so aus:StringBuilder resultString = new StringBuilder(); String separator = ""; for(MyType o :myCollection){ resultString.append(separator); resultString.append(o.toString()); separator = " and "; } Die zusätzliche (in den meisten Iterationen überflüssige) Zuweisung ist immer billiger, als das Neubefüllen der Pipeline und in der Schleife ist sie die billigste Anweisung…

bye
TT

[QUOTE=Sym]Ich wusste gar nicht, das manche Leute denken, final würde die Performance verbessern.

Ich konnte mich in dem Artikel nicht wiederentdecken. Eventuell sind diese “Mythen” wirklich nur bei Neulingen vorhanden - oder sie sind an mir komplett vorbei gegangen.[/QUOTE]

Beim final müsste mal geschaut werden wann diese Theorie auftauchte. Vll. hat es in den 90ern, als die VM etc. noch nicht so weit entwickelt war, etwas gebracht.

@Timothy_Truckle
Generell kann man sagen, dass Streams allgemein langsamer sind, als normale Schleifen. Das liegt aktuell daran, dass der Compiler hier noch nicht die Optimierungen vornimmt, die bei Schleifen durchgeführt werden. Ich glaube auf JaxEnter gab es dort vor kurzem ein Video zu diesem Thema.

Meist ist dies jedoch nicht relevant. Und auch wenn man dein Beispiel mit den zwei Schleifen auch durch einen Stream ersetzen könnte, ist auch dies meist nicht für die Performance relevant.

Generell halte ich nicht viel von Performance-Optimierungen ohne wirklichen Grund. In den meisten Fällen reicht es, sauberen Code zu schreiben. Und wenn dann Performanceoptimierungen vorgenommen werden müssen, kann man analysieren und diese Stellen dediziert anpassen.

[quote=Sym]Generell halte ich nicht viel von Performance-Optimierungen ohne wirklichen Grund.[/quote]Tatsächlich werden die schlimmsten Sünden in Sache Performanz (und Lesbarkeit) gemacht, weil man “schnellen” Code schreiben will, ohne sich die Mühe zu machen, die tatsächlichen Bottlenecks zu suchen.

[quote=Sym;130795]In den meisten Fällen reicht es, sauberen Code zu schreiben. Und wenn dann Performanceoptimierungen vorgenommen werden müssen, kann man analysieren und diese Stellen dediziert anpassen.[/quote]Da stimme ich Dir zu.

Andererseits ist gerade die Lesbarkeit eine Frage des persönlichen Programmierstils. Und da kann (und sollte) man sich an so simplen Grundsätzen die “Am schnellsten tut man das, was man nicht tut!” orientieren. Wer gewohnt ist, bei Bedarf einen Lambda-Ausdruck zu “expandieren” statt einen neuen Stream auf zu machen, bei dem wird das im Zweifelsfall auch nicht zu einem Performanzproblem werden, wenn die Collections doch mal größer sind…

Aber wie Du schon richtig sagtest: Lesbarkeit ist immer wichtiger als “premature optimization”!

bye
TT

die Bevorzugung von String-Konkatenation statt StringBuilder in den Tipps ist ja bemerkenswert,
vor allem da nicht auf den möglichen gigantischen Nachteil bei Schleifen eingegangen wird,
eines der klarsten theoretischen Performance-Probleme, die im Kleinen oft genug auch auftreten

einmal doch Schleifen kurz angesprochen und dann nur

Wenn das Zusammenbauen von Strings allerdings komplizierter wird und Schleifen oder Bedingungen enthält, dann stoßen die Optimierungen des Java-Compilers zurzeit an Grenzen, und das explizite Verwenden eines StringBuilder kann schneller sein.


wie deutlich wäre dagegen ein Satz: „10.000 Strings in einer Schleife per StringBuilder zusammengebaut ist aber übrigens ca. 10.000x schneller als mit Konkatenation“…

gibt es das eigentlich irgendwo, dass ein Compiler/ eine Runtime das optimiert?
eine simple Schleife mit stringVariable += neuerString?

solange diese große Gefahr besteht wäre fast vertretbar, auch bei der einfachen Variante glatt zu lügen, StringBuilder anzupreisen :wink:
aber da reicht natürlich einfaches +

und bei langen Strings ohne Schleife wie SQL über viele Zeilen bevorzuge ich die Schreibweise

            s += " aalsdjsdaljsadjalj ";
            s += " aalsdjsdaljsadjalj ";
            s += " aalsdjsdaljsadjalj ";
            s += " aalsdjsdaljsadjalj ";
            s += " aalsdjsdaljsadjalj ";
            s += " aalsdjsdaljsadjalj ";
            s += " aalsdjsdaljsadjalj ";
            s += " aalsdjsdaljsadjalj ";
            s += " aalsdjsdaljsadjalj ";
            s += " aalsdjsdaljsadjalj ";
            s += " aalsdjsdaljsadjalj ";
            s += " aalsdjsdaljsadjalj ";
            s += " aalsdjsdaljsadjalj ";
            s += " aalsdjsdaljsadjalj ";
            s += " aalsdjsdaljsadjalj ";
            s += " aalsdjsdaljsadjalj ";
            s += " aalsdjsdaljsadjalj ";
            s += " aalsdjsdaljsadjalj ";

auch da ist die StringBuilder-Variante bei mir bereits um Faktor 5 in diesem Beispiel schneller…,
aber die einfachere Variante noch wegen besserer Übersichtlichkeit vertretbar

[quote=SlaterB]String s = ""; s += " aalsdjsdaljsadjalj "; s += " aalsdjsdaljsadjalj "; s += " aalsdjsdaljsadjalj "; s += " aalsdjsdaljsadjalj "; s += " aalsdjsdaljsadjalj "; s += " aalsdjsdaljsadjalj "; s += " aalsdjsdaljsadjalj "; s += " aalsdjsdaljsadjalj "; s += " aalsdjsdaljsadjalj "; s += " aalsdjsdaljsadjalj "; s += " aalsdjsdaljsadjalj "; s += " aalsdjsdaljsadjalj "; s += " aalsdjsdaljsadjalj "; s += " aalsdjsdaljsadjalj "; s += " aalsdjsdaljsadjalj "; s += " aalsdjsdaljsadjalj "; s += " aalsdjsdaljsadjalj "; s += " aalsdjsdaljsadjalj ";[/quote]Na dann schau mal, was passiert, wenn Du das so schreibst:String s = "" + " aalsdjsdaljsadjalj "; + " aalsdjsdaljsadjalj "; + " aalsdjsdaljsadjalj "; + " aalsdjsdaljsadjalj "; + " aalsdjsdaljsadjalj "; + " aalsdjsdaljsadjalj "; + " aalsdjsdaljsadjalj "; + " aalsdjsdaljsadjalj "; + " aalsdjsdaljsadjalj "; + " aalsdjsdaljsadjalj "; + " aalsdjsdaljsadjalj "; + " aalsdjsdaljsadjalj "; + " aalsdjsdaljsadjalj "; + " aalsdjsdaljsadjalj "; + " aalsdjsdaljsadjalj "; + " aalsdjsdaljsadjalj "; + " aalsdjsdaljsadjalj "; + " aalsdjsdaljsadjalj ";
bye
TT

dass es so wieder schnell ist ist ja klar, bitte nicht übertrieben wenig Intelligenz unterstellen :wink:
nur mit kurzen und langen Zeilen haut es der Formatter durcheinander…, leider nicht praktikabel

                   + " aalsdjsdaljsadjalj " + " aalsdjsdaljsadjalj " + " aalsdjsdaljsadjalj " + " aalsdjsdaljsadjalj "
                   + " aalsdjsdaljsadjalj " + " aalsdjsdaljsadjalj " + " aalsdjsdaljsadjalj " + " aalsdjsdaljsadjalj "
                   + " aalsdjsdaljsadjalj " + " aalsdjsdaljsadjalj " + " aalsdjsdaljsadjalj " + " aalsdjsdaljsadjalj "
                   + " aalsdjsdaljsadjalj " + " aalsdjsdaljsadjalj ";

vielleicht gibt es dazu Formattereinstellung, vielleicht auch nicht,
die andere Variante funktioniert weltweit auf jeder IDE garantiert fehlerfrei

außerdem sind die Zeilen einzeln leicht zu kopieren, in ifs einzufügen usw.

[OT][quote=SlaterB]vielleicht gibt es dazu Formattereinstellung, vielleicht auch nicht,[/quote]

bye
TT

[QUOTE=Timothy_Truckle;130793][…]
Deshalb sehen meine String-Konkatenierungsschleifen so aus:StringBuilder resultString = new StringBuilder(); String separator = ""; for(MyType o :myCollection){ resultString.append(separator); resultString.append(o.toString()); separator = " and "; }
[…]
[/QUOTE]

Kannst du heutzutage etwas abkürzen mit

        for(MyType o : myCollection) {
            joiner.add(o.toString());
        }

Oder alternativ siehe Post https://forum.byte-welt.net/java-forum/allgemeine-themen/18852-java8-foreach-zustand-ausserhalb-post130822.html#post130822. Hatte ganz vergessen das es noch einen Collector fürs String-joinen gibt.

Argument: Final nicht schneller weil zur Laufzeit nicht mehr vorhanden?
Gegenfrage: Und was sind mögliche Auswirkungen beim compilieren?

Hatte mal einen Artikel gelesen wie man Java Objekte auf dem Stack erzeugt. Final ist nicht unwichtig.

[quote=TMII]Gegenfrage: Und was sind mögliche Auswirkungen beim compilieren?
[…]Final ist nicht unwichtig.[/quote]Dann kann man sich die finals ja von der IDE in den Code generieren lassen, wenn das Compilieren zu lange dauert…

bye
TT

Vielleicht auch das Gegenteil: Eher bei „Altlingen“. Leute, die zwischen 1990 und 1995 C programmiert haben, und dann 1996 mal irgendein trivial-Programm mit C und Java 1.1 per Stoppuhr miteinander verglichen haben, um festzustellen, dass Java langsamer war. Und heute bauen diese Leute Programme aus C-Code (den sie „C+±Code“ nennen, weil er Klassen enthält :rolleyes: ) und rechtfertigen das mit der „Performance“…

[QUOTE=Timothy_Truckle;130796]Tatsächlich werden die schlimmsten Sünden in Sache Performanz (und Lesbarkeit) gemacht, weil man „schnellen“ Code schreiben will, ohne sich die Mühe zu machen, die tatsächlichen Bottlenecks zu suchen.
[/QUOTE]

In bezug auf die Performance bin ich da nicht sicher. Allgemein entstehen die schlimmsten Sünden IMHO, wenn man versucht, einen Fehler zu beheben, dessen Ursache man nicht genau kennt. (Das könnte man aber mit der schwammigen Interpretation von „Fehler“ = „Niedrige Performance“ und „Behebung“ = „Optimierung“ darauf übertragen).

Bottlenecks zu suchen ist aber leider extrem schwierig. Wenn man etwas hat, wo es wirklich um einen zeitkritischen „Kern“ geht, dann kann man sich ja mal anschauen, was

  • Manuelle Zeitmessung mit System.nanoTime
  • jVisualVM mit Sampler
  • jVisualVM mit Profiler
  • Flight Recorder von Java Mission Control
    so liefern. In vielen Fällen werden die Ergebnisse völlig unterschiedlich sein - das hilft dann auch nur bedingt weiter…