Moin,
ich hab neulich was gelesen über Performance bei String-Operationen im Zusammenhang mit Java 1.4 und Java 5.
Demnach nutzen die StringBuffer in 5 nicht mehr den shared-char[] für die Methode toString(), was zu Performanceeinbußen for allem bei großen Strings im StringBuffer führt. Das leuchtet auch ein, da ja jedesmal der char-Array komplett kopiert werden muss.
Also ich einen mini-Test geschrieben mit 100.000 String-Operationen ((String += "trallalla") und StringBuffer.append()).
Das Ganze dann mit Version 1.4 und Version 5 ausgeführt und voll den Schock bekommen:
Die String-Operationen sind unter Java 5 ALLE um den Faktor 10 langsamer, als in Java 1.4!!! Auch das +=.
Hat da jemand ne Erklärung für?
Auch der StringBuffer mit append, also ohne toString() ist deutlich langsamer, als der gleiche Code unter 1.4.
Schon mal wer mit dem Problem zu tun gehabt?
Moin,
also Problem wäre für mich zum Beispiel, dass sämtliche XML-APIs massiv an Performance einbüßen müssten, wenn sich die StringOperationen alle so verlangsamt haben.
Mit verschiedenen VMs hab ich das noch nicht getestet. Mit dem StringBuilder das werd ich mal testen.
Das war mein Test-Quellcode:
Code:
public static void main(String[] args){
Date date1 = new Date();
Date date2 = null;
String s = new String();
for (int i = 0; i < 10000; i++) {
s += "sers";
}
date2 = new Date();
System.out.println("10.000 'sers' add to String: " + (date2.getTime() - date1.getTime()));
date1 = new Date();
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 10000; i++) {
sb.append("sers");
s = sb.toString();
}
date2 = new Date();
System.out.println("10.000 'sers' add to StringBuffer: " + +(date2.getTime() - date1.getTime()));
}
Wie gesagt, die Ausführungsgeschwindigkeit, selbst wenn man das sb.toString() auskommentiert, ist heftigst verschieden (1.4 und 5)...
So, jetzt habe ich den StringBuilder auch noch mal probiert:
Code:
StringBuilder sBuilder = new StringBuilder();
date1 = new Date();
for (int i = 0; i < 30000; i++) {
sBuilder.append("sers");
//s = sBuilder.toString();
}
date2 = new Date();
System.out.println("10.000 'sers' add to StringBuilder: " + +(date2.getTime() - date1.getTime()));
Also ich hab alle Varianten mal mit 30.000 Schleifendurchläufen versehen und siehe da: der StringBuilder ist langsamer, als der StringBuffer. Was sagt man dazu...
was hast du denn für einen lahmen Rechner, dass du bei 10k oder 30k irgendwas bemerkst,
+-30ms musst du immer rechnen, da fällt die 1ms Berechnungszeit nicht wirklich ins Gewicht
hier mal etwas aussagekräftigeres:
Code:
public class Test
{
static int k = 300000;
public static void main(String[] args)
{
// normal();
buffer();
builder();
buffer();
builder();
}
public static void normal()
{
Date date1 = new Date();
String s = new String();
for (int i = 0; i < k; i++)
{
s += "sers";
}
Date date2 = new Date();
System.out.println("String : " + (date2.getTime() - date1.getTime()));
}
public static void buffer()
{
Date date1 = new Date();
for (int j = 0; j < 100; j++)
{
StringBuffer sb = new StringBuffer();
for (int i = 0; i < k; i++)
{
sb.append("sers");
// s = sb.toString();
}
}
Date date2 = new Date();
System.out.println("StringBuffer : " + +(date2.getTime() - date1.getTime()));
}
public static void builder()
{
Date date1 = new Date();
for (int j = 0; j < 100; j++)
{
StringBuilder sBuilder = new StringBuilder();
for (int i = 0; i < k; i++)
{
sBuilder.append("sers");
// s = sBuilder.toString();
}
}
Date date2 = new Date();
System.out.println("StringBuilder: " + +(date2.getTime() - date1.getTime()));
}
}
-----
30 Mio. appends, Ausgabe:
StringBuffer : 6216
StringBuilder: 6137
StringBuffer : 6310
StringBuilder: 6200
ob nun synchronisiert oder nicht ist praktisch egal,
zu Java 1.4 kann ich nix sagen,
wenn du nur mit 10k append getestet hast, dann jedenfalls stark zu bezweifeln
Ok, ich nehm alles zurück und behaupte das Gegenteil. Ich hab als erstes immer den Test mit den einfachen Strings am laufen gehabt, das war wohl ein Fehler. Durch die String-Verknüpfungen sind wahrscheinlich so viele Objekte auf dem Speicher erstellt worden, dass nachher die Garbage-Collection die nachfolgenden Tests 'leicht' beeinflußt hat.
@Slater
So lahm is mein Rechner gar nich, ich komm ohne die String-Verknüpfungen fast auf die gleichen Zeiten, wie Du.
Ok, also Thread schließen und vergessen, der Fehler lag wie meist zwischen den Ohren.
Bei solchen Benchmarks ist immer etwas Vorsicht angebracht, schon weil der Rechner ja evtl. noch etwas anderes zu tun bekommt. Und je nach Größe und Anzahl der in den Operationen allokierten Objekte misst man evtl. auch irgendwann nur noch die GarbageCollection.
Auf jeden Fall ist es sinnvoll, für so einen Vergleich noch eine äußere Schleifen laufen zu lassen und so irgendwelche zufälligen Einflüsse besser auszumitteln.
Beim StringBuilder und beim StringBuffer zahlt es sich oft aus, ihn gleich so groß anzulegen wie man ihn am Ende braucht; dadurch spart man sich das dynamische Vergrößern.
Code:
public class StrPerf {
public static void main(String[] args) {
long t0 = 0;
long s1 = 0;
long s2 = 0;
long s3 = 0;
long s4 = 0;
long s5 = 0;
long s6 = 0;
long l = 0;
String s;
final int loop = 20;
final int cnt = 10000;
final int siz = 4*cnt;
for ( int i=0; i<loop; i++) {
t0 = System.currentTimeMillis();
s = new String();
for (int j = 0; j < cnt; j++) {
s += "sers";
}
s1 += (System.currentTimeMillis()-t0);
l += s.length();
s = new String();
for (int j = 0; j < cnt; j++) {
s = s.concat( "sers");
}
s2 += (System.currentTimeMillis()-t0);
l += s.length();
t0 = System.currentTimeMillis();
StringBuffer sbf = new StringBuffer();
for (int j = 0; j < cnt; j++) {
sbf.append("sers");
}
s = sbf.toString();
s3 += (System.currentTimeMillis()-t0);
l += s.length();
t0 = System.currentTimeMillis();
StringBuilder sbl = new StringBuilder();
for (int j = 0; j < cnt; j++) {
sbl.append("sers");
}
s = sbl.toString();
s4 += (System.currentTimeMillis()-t0);
l += s.length();
t0 = System.currentTimeMillis();
StringBuffer sbfi = new StringBuffer( siz);
for (int j = 0; j < cnt; j++) {
sbfi.append("sers");
}
s = sbfi.toString();
s5 += (System.currentTimeMillis()-t0);
l += s.length();
t0 = System.currentTimeMillis();
StringBuilder sbli = new StringBuilder( siz);
for (int j = 0; j < cnt; j++) {
sbli.append("sers");
}
s = sbl.toString();
s6 += (System.currentTimeMillis()-t0);
l += s.length();
}
System.out.println( "+=: " + s1);
System.out.println( "concat: " + s2);
System.out.println( "StringBuffer(default): " + s3);
System.out.println( "StringBuilder(default):" + s4);
System.out.println( "StringBuffer(alloc'd): " + s5);
System.out.println( "StringBuilder(alloc'd):" + s6);
}
}
Auf meinem (betagten) Rechner ergibt das (zweimal mit Hotspot-VM, zweimal mit Server-VM):
Ich würde daraus nur folgende Schlüsse ziehen
a) concat ist etwas langsamer als +=
b) concat und += sind erheblich langsamer als StringBuffer/StringBuilder
c) ob StringBuffer oder StringBuilder, macht kaum einen messbaren Unterschied
d) die Vorallokation des Buffers bringt augenscheinlich nicht sehr viel
e) für die mit der Erzeugung vieler temporärer Objekte verbundenen Asätz mit += und concat hat die Server-VM die Nase vorn
also deine Aussagen c und d sind ja erstaunlich,
du sagst es selber und ich habe es doch hier im Thread auch schon erwähnt:
+-30ms sind reiner Zufall, was macht es für einen Sinn 48ms zu messen?
lasse += und concat weg und verlängere die Messung um den Faktor 100-1000, dann wirds langsam sichtbar
also deine Aussagen c und d sind ja erstaunlich,
du sagst es selber und ich habe es doch hier im Thread auch schon erwähnt:
+-30ms sind reiner Zufall, was macht es für einen Sinn 48ms zu messen?
lasse += und concat weg und verlängere die Messung um den Faktor 100-1000, dann wirds langsam sichtbar
Da habe ich mich wohl falsch ausgedrückt - ich meinte, dass genau diese Ergebnisse keine weiteren Schlüsse zulassen. Wenn man genauer feststellen willen, was z.B. die Vorallokation bringt, dann muss man genauer messen.
naja, was macht es für einen Sinn, so ein ungenaues Programm zu schreiben und dann 'macht kaum einen messbaren Unterschied' zu schreiben,
das klingt eher, als wenn das einfache System.currentTimeMillis(); nicht hilft/ man ganz andere Testmethodenbraucht,
als dass man einfach zwei Nullen einfügen muss,
eine minimale Programmänderung um sehr viel genauere Ergebnisse zu erhalten
greife das thema nochmal auf, wenn ich das programm von slaterb so abänder, das 1000 string pro methode erzeugt werden, dann bekomme ich relative aussagekräftige werte:
Code:
import java.util.*;
public class Test
{
static int k = 3000;
public static void main(String[] args)
{
System.out.print("1 ");
normal();
System.out.print("2 ");
buffer();
System.out.print("3 ");
builder();
System.out.print("4 ");
buffer();
System.out.print("5 ");
builder();
}
public static void normal()
{
Date date1 = new Date();
for (int j = 0; j < 1000; j++)
{
String s = new String();
for (int i = 0; i < k; i++)
{
s += "sers";
}
}
Date date2 = new Date();
System.out.println("String : " + (date2.getTime() - date1.getTime()));
}
public static void buffer()
{
Date date1 = new Date();
for (int j = 0; j < 1000; j++)
{
StringBuffer sb = new StringBuffer();
for (int i = 0; i < k; i++)
{
sb.append("sers");
String s = sb.toString();
}
}
Date date2 = new Date();
System.out.println("StringBuffer : " +(date2.getTime() - date1.getTime()));
}
public static void builder()
{
Date date1 = new Date();
for (int j = 0; j < 100; j++)
{
StringBuilder sBuilder = new StringBuilder();
for (int i = 0; i < k; i++)
{
sBuilder.append("sers");
String s = sBuilder.toString();
}
}
Date date2 = new Date();
System.out.println("StringBuilder: " +(date2.getTime() - date1.getTime()));
}
}
ändere mal
> buffer() for (int j = 0; j < 1000; j++)
> builder() for (int j = 0; j < 100; j++)
wieder zu einem fairen Zweikampf,
dann hast du wieder annähernd die gleiche Zeit..
Ich bin auf diesen Thread gestossen, weil ich gerne mal nach Links auf meine Seiten suche. Also hier den Link auf meinen Artikel über den Speicherverbrauch in Zusammenhang mit StringBuffer und String.
Ich habe mich mit dem Thema aufgrund von Speicherproblemen beschäftigt und nicht aufgrund von Performance.
Aber vielleicht mal zum allgemeinen Verständnis:
Intern wird die Operation "+" auf Strings durch StringBuffer "append" realisiert, ausser bei bereits zur Compile Time realisierbaren Konkatenierungen, diese werden bereits vom Compiler zusammengefasst.
Daher gibt es hier weder in der Performance noch im Speicherverbrauch Unterschiede, jedenfalls bei einer einzelnen Ausführung.
Anders sieht es in Schleifen aus. Da in einer Schleife bei jeder Konkatenierung mit "+" wieder intern ein neuer StringBuffer angelegt wird, was aufwendiger ist als bei einem einzelnen StringBuffer immer wieder neu anzuhängen.
Ein weiterer Effekt dabei ist, dass beim Anhängen an einen StringBuffer der interne Speicher verdoppelt wird, d.h. nicht jede "append" Operation führt zu einer internen Vergrösserung des Arrays mit anschliessendem Umkopieren, sondern nur dann, wenn die Speichergrösse überschritten wird. Da die Vergösserung durch Verdoppellung geschieht wächst der verfügbare Speicher sehr schnell, wenn man also sehr oft kurze Strings anfügt, wird nur sehr selten der Speicher vergrössert und umkopiert. Bei der "+" Methode wird jedesmal ein StringBuffer generiert, dann intern der Speicher vergrössert und dann das Array intern umkopiert.
Hier ist also StringBuffer dem normalen String in der Performance überlegen.´
Mein Problem war dabei aber nie die Performance, sondern die schlechte Speicherplatznutzung.
Wenn man einen String hat der bereits sehr lang ist, und dann einen kurzen String anhängt, dann verdoppelt sich im schlimmsten Fall der verbrauchte Speicher.
Angenommen wir haben folgenden Fall:
Code:
char arr[]=new char[16000];
... was reinschreiben
String test=new String(arr);
test=test+"abc";
Dann wird intern das Array auf 32000 Zeichen vergrössert, obwohl nur 3 Zeichen angehängt werden.
Eine Besonderheit ist dabei, dass die StringBuffer.toString() Methode bis JDK 1.4 nicht das interne char[] kopiert, sondern den String das interne Array zuweist, so dass im Beispiel oben der String test auch weiterhin 32000 Zeichen intern enthält, obwohl nur 16003 Zeichen benötigt werden.
Ab JDK 1.5 wird bei toString() eine Kopie des internen char[] angelegt, das genau so gross ist wie der tatsächlich benötigte Speicherplatz.
Im Prinzip funktioniert die toString() Routine ab JDK 1.5 wie die .substring(0) Routine bis JDK 1.4.
Also wird ab JDK 1.5 ein char[] mehr angelegt und ein System.arraycopy mehr ausgeführt.
Die Konkatenierung ist somit im JDK 1.5 nicht langsamer, wohl aber die Methode toString().
Das hat dann wiederum eine Auswirkung auf die + Methode bei Strings, da intern ja bei jedem + ein append und ein toString ausgeführt wird. Der Performanceverlust kommt also durch die neue toString Methode, hat aber den Vorteil, dass der Speicherplatzverbrauch geringer ist.
Bei künstlichen Tests merkt man diesen Unterschied deutlich, aber in wirklichen Applikationen hilft die günstigere Speicherplatznutzung widerum beim garbage collection (aber das ist ein kompliziertes Thema, über das ich vielleicht auch mal einen Artikel schreibe).
StringBuilder und StringBuffer unterscheiden sich dann nur noch darin, dass StringBuilde nicht synchronisiert ist. Das bringt bei normalen Desktop-Applikationen so gut wie nichts, da diese meist mehr oder weniger Single threaded sind, d.h. man hat auch nicht allzuviele Monitore, die man "synchronisieren" muss. Anders sieht es bei Server-Applikationen aus, z.B. bei Servlets mit vielen konkurrierenden Threads. Allerdings bringt das auch nicht wirklich viel.
Ich hoffe mal, dass war wenigstens etwas hilfreich.