Dienstag, 25. Juni 2013

GWT Performance-Tuning: Optimierung von Arrays

In meinem letzten Beitrag habe ich gezeigt, wie man bei GWT die Performance einzelner Methoden ermitteln kann. Davon ausgehend möchte ich heute an Hand eines Beispieles zeigen, wie man die Performance eines GWT-Programmes deutlich verbessern kann.

Thematisch geht es um das Tuning von Java-Arrays in GWT.

Werfen wir noch mal einen Blick auf die Ausgangslage, d. h. die von Firebug ermittelten Ausführungszeiten:

Bei dem Beispiel handelt es sich um ein Spiel (Reversi, um genau zu sein, spielt aber für das Beispiel keine Rolle), bei dem der beste Zug des Computers dadurch ermittelt wird, dass alle möglichen Züge des Computers rekursiv durchprobiert werden.  Mit steigender Rechentiefe steigt die Zahl möglicher Züge und damit die Rechenzeit exponentiell an.

Die fünf Funktionen mit dem höchsten Verbrauch an Rechenzeit sind wrapArray(), initDims_0, arraycopy(), $istErlaubt() und $pr(). Dabei sind die von mir geschriebenen Funktionen $istErlaubt() und $pr() elementar an der Überprüfung der Stellungen beteiligt, es war also zu erwarten, dass sie weit vorne in der Liste auftauchen. Die anderen drei Methoden stammen jedoch aus der GWT-Bibliothek. Ein kurzer Blick in den von GWT erzeugten JavaScript-Code (mit Option "pretty" erzeugt) zeigt, dass alle drei Funktionen beim Erzeugen von Arrays verwendet werden

Arrays, die ja eigentlich nicht gut in das Konzept der Sprache Java passen, da sie keine normalen Objekte sind, sind dennoch in den Java-Sprachstandard aufgenommen worden, da sie von der virtuellen Maschine sehr effizient ausgeführt werden können. Dies scheint für den von GWT erzeugten JavaScript-Code nicht zu gelten. Letztlich verwundert dies nicht, denn JavaScript-Arrays sind zwar flexibel, aber eher langsam und konzeptionell anders als Java-Arrays.

1. Optimierung: Zweidimensionale Arrays zu eindimensionalen Arrays

Das in dem Beispiel betrachtete Programm implementiert ein Spiel, welches auf einem Brett von acht mal acht Feldern gespielt wird. Die logischste Implementierung der Definition im Programmcode ist
    private Farbe spiel[][];
(Farbe gibt an, wem der auf dem Feld befindliche Stein gehört; ist für das Beispiel ohne Bedeutung), der Zugriff auf einzelne Elemente erfolgt über
    wert = spiel[reihe][spalte];
Das Programm legt in großem Umfang neue Arrays an, z. B. bei jeder zu prüfenden Stellung im Spiel.

Zweidimensionale (oder entsprechend mehrdimensionale) Arrays in Java haben die Besonderheit, dass jede Reihe unterschiedlich viele Spalten enthalten kann. Ein Zweidimensionales Java-Array ist also ein Array von Arrays, bei denen die Länge nicht festgelegt ist. (Im Gegensatz dazu hat in C jede Reihe gleich viel Spalten, was bei Verlust an Flexibilität den Zugriff beschleunigt. Ein C-Array ist auch nur ein einziges Objekt.) Diese Flexibilität wird hier aber nicht gebraucht, sondern sie ist sogar schädlich, da sie die Anzahl zu erzeugender Objekte erhöht, und gerade diese Objekte (die einzelnen Arrays der Reihen) sind in JavaScript nur kostspielig zu erzeugen. Das zeigt sich auch im Java-Code schon, z. B. bei der Kopie eines solchen Arrays
    spiel = new Farbe[8][8];
    for (int s = 0; s < 8; s++) {
      System.arraycopy(original[s], 0, spiel[s], 0, 8);
    }

(und da haben wir mit arraycopy auch schon eine der Funktionen, die viel Rechenzeit verbraucht).

Für die Optimierung habe ich jetzt die zweidimensionalen Arrays in eindimensionale Arrays umgewandelt, statt 8x8 Elemente jetzt 64 Elemente.

Aus der Variablendefinition wird jetzt
  private Farbe spiel[];
der Zugriff wird mit der statischen Funktion
  public static final int xy(int x, int y) {
    return y * ANZAHL_REIHEN + x;
  }
zu
   wert = spiel[Brett.xy(reihe, spalte)]
(Der zusätzliche Funktionsaufruf kostet übrigens keine Performance, da er von GWT in Inline-Code umgewandelt wird.)

Die Kopie eines Arrays wird dadurch deutlich schlanker:
    spiel = new Farbe[64];
    System.arraycopy(original, 0, spiel, 0, 64);

Wesentlich ist jedoch, dass für ein Spielfeld (eine zu prüfende Stellung) nur noch ein JavaScript-Array und nicht mehr acht angelegt werden müssen.

Eine erneute Messung der Ausführungszeiten in Firebug ergibt jetzt folgendes Bild:
Die Reihenfolge der Funktionen hat sich geändert. Die drei intensiv an der Arrayerzeugung beteiligten Funktionen wrapArray(), initDims_0 und arraycopy() benötigen zusammen nur noch 34,4% statt wie vorher 46,21%. Die anderen Funktionen benötigen jetzt prozentual mehr Zeit, ist aber auch logisch, da die Summe immer 100% sind. Da wir an den anderen Funktionen nichts geändert haben, bedeutet dies, dass unser Programm effektiv schneller geworden ist. (Mit etwas Mathe ergibt sich, dass das Programm nur noch 82% der bisherigen Rechenzeit braucht oder mit anderen Worten um 22% schneller geworden ist.)

2. Optimierung: Arrays recyclen

Mit der letzten Optimierung wurde zwar bereits eine erhebliche Verbesserung erreicht, aber 34,4% für das Array-Handling sind immer noch viel. Für eine weitere Optimierung ist ein Blick auf den Algorithmus im Programm notwendig.

Das Programm berechnet den besten Zug im Spiel, indem mögliche Stellungen rekursiv durchprobiert werden. Je weiter die Berechnung in die Tiefe geht, desto mehr potentielle Stellungen gibt es. Für jede durchprobierte Stellung werden Arrays angelegt, die nach durchprobieren der Stellung wieder verworfen werden. Es werden also viele Arrays erzeugt, pro rekursiver Rechentiefe wird aber (pro Variable) nur ein Array wirklich benutzt.

Die Lösung für die Optimierung besteht nun darin, dass eine gewisse Anzahl von Arrays in einem Puffer vorgehalten werden, bei Bedarf hieraus eine Instanz angefordert wird und diese auch wieder explizit freigegeben wird.

So wird im Konstruktor aus spiel = new Farbe[64]; jetzt
    spiel = neuFarbe64();
Man sollte daran denken, dass das Array jetzt nicht frisch initialisiert ist, sondern irgendwelche zufälligen Werte enthalten kann.
Zusätzlich wird eine Art Destruktor gebraucht (der Begriff ist hier nicht ganz korrekt, es handelt sich nicht um einen vom System automatisch aufgerufenen Destruktor [finalize() von Java ist nicht geeignet, da der Aufruf zu spät kommt], sondern eine Funktion, die explizit aufgerufen werden muss zum Freigeben der Ressourcen - danach darf die Instanz nicht mehr benutzt werden).
    public void freigeben() {
      if (spiel != null) {
        freigebenFarbe64(spiel);
        spiel = null;
      }

      /* weitere Ressourcen werden hier freigegeben */
    }



Angewendet sieht das dann so aus:
    Stellung neueStellung = new Stellung(...);
    /* mache irgendetwas mit der Instanz neueStellung */
    neueStellung.freigeben();
    /* die Instanz neueStellung darf nicht mehr benutzt werden.


Jetzt fehlen nur noch die zwei Methoden zum Verwalten des Ressourcen-Pools:
    private static Farbe farbe64Array[][] = new Farbe[MAX_RECHENTIEFE + n][];
    private static int farbe64Zeiger = 0;
 

    private static Farbe neuFarbe64()[] {
      if (farbe64Zeiger > 0) {
        return farbe64Array[--farbe64Zeiger];
      } else {
        return new Farbe[64];  /* erstmalige Erzeugung */
      }
    }
 
    private static void freigebenFarbe64(Farbe array[]) {
      assert array.length == 64;
      assert farbe64Zeiger < farbe64Array.length;
      farbe64Array[farbe64Zeiger++] = array;
    }

Die Namensgebung lässt schon darauf schließen, dass für jeden optimierten Arraytyp ein eigenes Methodenpaar nötig ist.

Der Code ist jetzt leider weniger Java-like und etwas schwerer lesbar. Daher sollte es jetzt auch zu einer Verbesserung der Performance kommen, damit sich der Aufwand gelohnt hat.

Eine Messung ergibt diese Werte
Die beiden Funktionen wrapArray() und arraycopy() benötigen zusammen nur noch 11,96%, initDims_0() taucht überhaupt nicht mehr auf, was heißt, diese Funktion benötigt weniger als 0.04%. Für das Array-Handling haben wir jetzt nur noch 12% statt 34,4% oder 46,21%. Dies heißt, dass noch 74.5% der Rechenzeit der Version mit erster Optimierung und 61% der Rechenzeit der ursprünglichen Version benötigt werden oder mit anderen Worten dass das Programm um 34% bzw. 63,6% schneller geworden ist. Dieser Geschwindigkeitsgewinn ist für den Benutzer ganz erheblich bemerkbar, bedeutet aber keine funktionale Einschränkung für ihn.


Fazit:

Arrays sind in Java zwar schnell, nicht aber in GWT. Die Erzeugung neuer Arrays kostet viel Zeit. Der Wechsel auf andere Objekte wie z. B. Listen löst das Problem nicht, da diese meistens ebenfalls intern auf Arrays basieren. Stattdessen sollte im Programmdesign überlegt werden, wie die Anzahl der Array-Erzeugungen gering gehalten wird. Dies betrifft nur Arrays, die in oft durchlaufenen Programmteilen erzeugt werden. Es spricht aber nichts gegen die eher statische Verwendung einer auch größeren Anzahl von Arrays.



Freitag, 14. Juni 2013

Performancemessung mit GWT

Mit GWT (Google Web Toolkit) lässt es sich für einen Java-Programmierer gut programmieren. Die bekannten Java-Klassen sind dort größtenteils vorhanden, die bekannten Algorithmen lassen sich meistens verwenden, doch wenn das Programm dann ausgeführt wird, kommt manchmal die Enttäuschung bezüglich der Geschwindigkeit. Die Umgebung ist deutlich anders, der Java-Code wird nicht in den Byte-Code einer virtuellen Maschine übersetzt, sondern nach JavasScript. JavaScript erreicht nicht die Geschwindigkeit des Byte-Codes, aber was wesentlich mehr ins Gewicht fällt: Die Tücken liegen an ganz anderer Stelle. Was in Java ansonsten performant ist, kann in JavaScript überraschend langsam sein.

Bevor das Programm getunt werden kann, müssen erst einmal die Schwachstellen aufgedeckt werden. Dazu braucht es ein Tool zur Performancemessung, also ein Tool, welches anzeigen kann wo wieviel Zeit verbraucht wurde.

Ich gehe auf zwei Tools ein:
  1. Google Speed Tracer
  2. Firebug mit der Zeitmessung

Speed Tracer werde ich nur kurz behandeln, da Firebug das wesentlich geeignetere Tool ist.

1. Messungen mit Speed Tracer


Speed Tracer ist ein Plugin für Chrome, welches von Google selbst entwickelt wird. Die Installation wird auf developers.google.com/web-toolkit/speedtracer gut beschrieben.

Einmal installiert kann Speed Tracer jederzeit durch einen Klick auf das kleine grüne Symbol einfach gestartet werden und produziert Grafiken, die hübsch anzusehen sind, leider jedoch wenig für uns geeignete Informationen bieten.
 
Sehen wir uns mal eine solche Grafik an:


Man erkennt sofort, dass sich das Programm zu 99,8% im JavaScript Callback befindet. Gleichzeitig ist dies die einzige verwertbare Information, die Speed Tracer in diesem Beispiel liefert. Als Programmiererin, sie ich das Programm selbst geschrieben habe, weiß ich das aber auch, schließlich kenne ich ja meinen Code.

Viel interessanter ist, in welchen Funktionen wie viel Rechenzeit verbracht wird. Diese Information kann Speed Tracer – soweit ich weiß – nicht bieten. Firebug kann das aber, und daher leite ich gleich dazu über.

2. Messungen mit Firebug


Firebug ist das Entwickler-Tool für den Firefox. Es bietet in der Konsole u. A. eine Zeitmessung auf der Ebene von JavaScript-Funktionen.


Damit uns die Ausgaben von Firebug nutzen, müssen wir GWT dazu bringen, lesbare Methodennamen in JavaScript zu generieren. Dies geht ganz leicht, indem wir beim GWT-Compilieren den „Output Style” auf „Pretty” setzen.

Jetzt gehen wir in den Firebug. Eventuell muss dort unter „Skript” das JavaScript neu laden. Anschließend können wir im Punkt „Konsole” über „Zeitmessung” unsere Messung nach Belieben ein- und ausschalten.

Nach getaner Arbeit erhalten wir ein Protokoll wie das Folgende:

Durch einen Klick auf die Spaltenüberschrift kann das Ergebnis sortiert werden. Am sinnvollsten ist eine Sortierung nach Prozent.

Schauen wir nur auf die ersten fünf Funktionen, da dort die meiste Rechenzeit verbraucht wird. Als Programmierer weiß ich, dass die Funktionen istErlaubt() und pr() von mir stammen. Die ersten drei Funktionen wrapArray(), initDims_0 und arraycopy() kommen aber scheinbar aus der GWT Bibliothek. Da sie zusammen 46% der Rechenzeit (dritte Spalte) verbrauchen, ist dies ein Grund, sie genauer anzuschauen. (Bei einer anderen Anwendung werden es natürlich ganz andere Funktionen sein, die vorne mit dem größten Zeitverbrauch stehen.)

Zum Untersuchen der Funktionen öffnen wir die Datei, die in der letzten Zeile angegeben ist. Bei unseren eigenen Funktionen können wir dort den von GWT erzeugten Code anschauen und überlegen, wo Verbesserungspotential ist. Bei den Bibliotheksfunktionen bringt das nichts, da wir deren Code nicht ändern können. Wir können uns aber anschauen, wo in unserem Code diese Funktionen direkt oder indirekt aufgerufen.

In dem Beispiel sind die drei am meisten aufgerufenen Funktionen bei der Erzeugung von Arrays beteiligt. Das lässt vermuten, dass ich in meinem Code ein Performanceproblem mit Arrays habe.

Wie dieses angegangen und der Code optimiert werden kann, werde ich im nächsten Beitrag meines Blogs zeigen.



Mittwoch, 12. Juni 2013

Was dieser Blog ist

Eigentlich bin ich keine Bloggerin. Auch sehe ich keinen Sinn darin, der ganzen Welt zu beschreiben, wie ich aufgestanden bin und gefrühstückt habe. Nicht dass es irgendwen interessieren würde. Aber das ist auch keineswegs der Sinn dieses Blogs.

Manchmal stoße ich bei der Arbeit - Software-Entwicklung - auf Probleme, für die auch Tante Google so keine Lösung kennt. Aber dennoch mag es sein, dass auch andere Programmierer vor den gleichen Herausforderungen stehen. Da wäre es schade, wenn ich meine Erkenntnisse für mich behalte. Umgekehrt hat mir mancher gute Technik-Blog schon sehr geholfen. Das Netz ist idealerweise Geben und Nehmen. In diesem Blog will ich auch etwas geben.

Die Themen werden daher um das kreisen, was ich selbst täglich mache und gut kann, und das sind Java mit ihren Freunden GWT (Google Web Toolkit), JSF (Java Server Faces), Hibernate und ein paar mehr.

Über positive und konstruktive Kritik zu meinen Artikeln werde ich mich sicher freuen.