Thursday, February 28, 2013

Performance bei der String Konkatenierung in Java

Ein immer wieder gern gemachter Fehler der zu schlechter Performance in Java Programmen führt ist, das Zusammenbauen von Strings mit dem "+" Operator. Vor allem wenn dieses in einer Schleife gemacht wird steigt der Speicherverbrauch enorm an und der Garbagge Collector läuft Amok. Ein kleines Beispiel. Wir haben eine Liste (List<Person>) mit 100 Namen und wollen diese in eine CSV Datei ausgeben. Folgender Code erledigt die Aufgabe zwar völlig richtig. Wenn man das Programm aber in einem Profiler laufen lässt sieht man, dass jede Menge Hauptspeicher verschwendet wird und das Programm die meiste Zeit mit Garbagge Collecting verbringt.

  String result = "\"Vorname\",\"Nachname\"\n";
  for (Person person : personen) {
   result = result + "\"" + person.getVorname() + "\",\"" + person.getNachname() + "\n";
  }
  System.out.println(result);

Warum ist das so? Die Erklärung liegt darin, dass ein String in Java unveränderbar ist. Das heißt es ist in Java nicht möglich, dass man einen String einfach zu einem anderen hinzufügt. Viel mehr wird bei jeder Konkatenierung ein neuer String erzeugt. Mit diesem Hintergrund sieht man sofort, dass in unserem Programm jede Menge sinnloser Strings erzeugt werden, die sofort wieder von der Garbagge Collection entsorgt werden müssen. Vor allem böse ist aber die "result=result+..." Anweisung. Den der result String wird mit jedem Schleifendurchlauf länger und länger und dadurch wird auch der Hauptspeicher der bei jedem Schleifendurchlauf für den String angefordert werden muß größer und größer. Bei 100 Namen wird das vielleicht noch nicht so viel ausmachen. Bei 1000 Namen kommen da aber gleich mal viele Garbagge Collection Zyklen zusammen und schon haben wir wieder ein Programm, dass die urban legend das Java langsam ist scheinbar bestätigt.

Doch natürlich besitzt die Javaklassenbibliothek mit der StringBuilder Klasse eine Lösung für das Problem. Mit dieser veränderlichen Klasse können fast beliebig lange Strings einfach und effizient zusammengebaut werden.

  StringBuilder result = new StringBuilder();
  result.append("\"Vorname\",\"Nachname\"\n");
  for (Person person : personen) {
   result.append("\"");
   result.append(person.getVorname());
   result.append("\",\"");
   result.append(person.getNachname());
   result.append("\n");
  }
  System.out.println(result.toString());

Mit dieser Variante haben wir die Anzahl der unnötig erstellen String Objekte dramatisch reduziert. Intern verwendet der StringBuilder ein char Array, dass immer wieder vergrössert wird. Das char Array ist standardmäßig mit 16 Zeichen intialisiert und bei jeder Erweiterung wird die Kapazität verdoppelt. Das ist in unserem Beispiel natürlich immer noch eine Verschwendung. Deshalb empfiehlt es sich den StringBuilder als Parameter eine Schätzung der maximalen Größe des StringBuilder mitzugeben. Dadurch kann man nocheinmal einen Performancegewinn erzielen.

Falls das Zusammensetzen des Strings aus mehreren Threads erfolgen soll, sollte man statt des StringBuilders die Klasse StringBuffer verwenden. Letztere ist nämlich im Gegensatz zu StringBuilder Threadsafe.

2 comments:

  1. Bin mal von jemanden angemeckert worden, das wäre inperformant:

    "x:"+x+" y:"+y;

    Was natürlich Unsinn ist, weil der Compiler das automatisch in

    new StringBuilder()).append("x:").append(x).append(" y:").append(y).toString();

    umwandelt. Und obige Zeile klar lesbarer ist.

    Wird der String nicht in einer Zeile sondern in Schleifen oder ähnlichem zusammengebaut immer StringBuilder oder StringBuffer verwenden.

    ReplyDelete
  2. Hallo Axel!

    Stimmte schon, dass der Compiler einfache String Konkatenierung wegoptimieren kann, aber gerade bei performancekritischen Codestellen gehe ich lieber auf Nummer Sicher.

    ReplyDelete

ad