2013-05-23 12 views
8

So war ich lead to believe, dass mit dem "+" Operator, um Strings an einer einzigen Zeile anhängen, war genauso effizient wie mit einem StringBuilder (und auf jeden Fall viel schöner auf die Augen). Heute hatte ich zwar Geschwindigkeitsprobleme mit einem Logger, der Variablen und Strings anhängte, aber er verwendete einen "+" - Operator. Also machte ich eine schnelle test case und zu meiner Überraschung stellte sich heraus, dass die Verwendung eines StringBuilders schneller war!Geschwindigkeit Unterschied für einzelne Zeile String Verkettung

Die Grundlagen sind der Durchschnitt von 20 Läufen für jede Anzahl von Anhängen, mit 4 verschiedenen Methoden (siehe unten).

Ergebnisse, Zeiten (in Millisekunden)

 
               # of Appends 
          10^1 10^2 10^3 10^4  10^5  10^6  10^7 
StringBuilder(capacity) 0.65 1.25 2  11.7  117.65 1213.25 11570 
StringBuilder()   0.7  1.2  2.4  12.15 122  1253.7  12274.6 
"+" operator    0.75 0.95 2.35 12.35 127.2 1276.5  12483.4 
String.format    4.25 13.1 13.25 71.45 730.6 7217.15 - 

Grafik der prozentualen Abweichung vom schnellsten Algorithmus.

% Difference in String timings

Ich habe die byte code ausgecheckt, es für jeden String-Vergleichsverfahren unterschiedlich ist.

Hier ist, was ich für die Methoden verwende, und Sie können die gesamte Testklasse here sehen.

Ich habe jetzt mit Floats, Ints und Strings versucht. Alle zeigen mehr oder weniger den gleichen Zeitunterschied.

Fragen

  1. Der Operator "+" werden immer eindeutig nicht den gleichen Byte-Code, und die Zeit ist sehr verschieden von der optimal. Also was gibt es?
  2. Das Verhalten der Algorithmen zwischen 100 und 10000 Anzahl der Anhänge ist sehr seltsam für mich, hat also jemand eine Erklärung?
+0

Das Verhalten der Algorithmen zwischen 100 und .... ??? – ChrisCM

+0

behoben, es aus irgendeinem Grund abgeschnitten – greedybuddha

+2

Große Frage, mit Forschung und Daten, um es zu sichern. +1 – syb0rg

Antwort

3

Die Java Language Specification ist nicht festgelegt, wie die String-Verkettung durchgeführt wird, aber ich bezweifle, dass Ihr Compiler tut alles andere als das Äquivalent von:

new StringBuilder("["). 
    append(a). 
    append(","). 
    append(b). 
    append(","). 
    append(c). 
    append("]["). 
    append(x). 
    append(","). 
    append(y). 
    append(","). 
    append(z). 
    append("]"). 
    toString(); 

Sie können „verwenden javap -c ... "dekompilieren Sie Ihre Klassendatei und überprüfen Sie dies.

Wenn Sie einen signifikanten und sich wiederholenden Unterschied in der Laufzeit zwischen Ihren Methoden messen, würde ich viel eher davon ausgehen, dass der Garbage Collector zu unterschiedlichen Zeiten ausgeführt wird, als dass es tatsächlich einen signifikanten Leistungsunterschied gibt. Das Erzeugen der StringBuilder s mit unterschiedlichen Anfangskapazitäten kann natürlich einige Auswirkungen haben, sollte jedoch im Vergleich zu dem Aufwand, der z. Formatiere die Floats.

+0

Ich habe den Code dekompiliert, und der Operator "+" ist anders als der StringBuilder(), obwohl ich nicht gegen StringBuilder ("["); versuchen, schauen Sie sich – greedybuddha

+0

Update: Der Bytecode ist sogar anders als der neue StringBuilder ("[") .append ... – greedybuddha

+0

Können Sie die Ausgabe von javap zu Ihrer Frage hinzufügen? – jarnbjo

4

Ich mochte zwei Dinge über Ihren Testfall nicht. Zuerst haben Sie alle Tests innerhalb desselben Prozesses ausgeführt. Im Umgang mit "groß" (mehrdeutig, ich weiß), aber wenn Sie sich mit etwas beschäftigen, wo Ihr Prozess mit der Erinnerung interagiert, ist Ihre Hauptsorge, sollten Sie immer Benchmark in einem separaten Lauf. Allein die Tatsache, dass wir den Müll gesammelt haben, kann die Ergebnisse früherer Läufe beeinflussen. Die Art und Weise, wie Sie Ihre Ergebnisse einkalkuliert haben, verwirrte mich irgendwie. Was ich getan habe, war, dass ich jeden einzelnen Run genommen habe und eine Null von der Anzahl der Male, die ich es gefahren bin. Ich lasse es auch für eine Reihe von "Wiederholungen" laufen, wobei jede Wiederholung zeitlich abgestimmt wird. Dann ausgedruckt die Anzahl der Millisekunden, die jeder Lauf dauerte.Hier ist mein Code:

import java.util.Random; 

public class blah { 
    public static void main(String[] args){ 
    stringComp(); 
    } 

    private static void stringComp() { 
     int SIZE = 1000000; 
     int NUM_REPS = 5; 
     for(int j = 0; j < NUM_REPS; j++) { 
      Random r = new Random(); 
      float f; 
      long start = System.currentTimeMillis(); 
      for (int i=0;i<SIZE;i++){ 
       f = r.nextFloat(); 
       stringSpeed3(f,f,f,f,f,f); 
      } 
      System.out.print((System.currentTimeMillis() - start)); 
      System.out.print(", "); 
     } 
    } 

    public static String stringSpeed1(float a, float b, float c, float x, float y, float z){ 
     StringBuilder sb = new StringBuilder(72).append("[").append(a).append(",").append(b).append(",").append(c).append("]["). 
       append(x).append(",").append(y).append(",").append(z).append("]"); 
     return sb.toString(); 
    } 

    public static String stringSpeed2(float a, float b, float c, float x, float y, float z){ 
     StringBuilder sb = new StringBuilder().append("[").append(a).append(",").append(b).append(",").append(c).append("]["). 
       append(x).append(",").append(y).append(",").append(z).append("]"); 
     return sb.toString(); 
    } 

    public static String stringSpeed3(float a, float b, float c, float x, float y, float z){ 
     return "["+a+","+b+","+c+"]["+x+","+y+","+z+"]"; 
    } 

    public static String stringSpeed4(float a, float b, float c, float x, float y, float z){ 
     return String.format("[%f,%f,%f][%f,%f,%f]", a,b,c,x,y,z); 
    } 

} 

Jetzt meine Ergebnisse:

stringSpeed1(SIZE = 10000000): 11548, 11305, 11362, 11275, 11279 
stringSpeed2(SIZE = 10000000): 12386, 12217, 12242, 12237, 12156 
stringSpeed3(SIZE = 10000000): 12313, 12016, 12073, 12127, 12038 

stringSpeed1(SIZE = 1000000): 1292, 1164, 1170, 1168, 1172 
stringSpeed2(SIZE = 1000000): 1364, 1228, 1230, 1224, 1223 
stringSpeed3(SIZE = 1000000): 1370, 1229, 1227, 1229, 1230 

stringSpeed1(SIZE = 100000): 246, 115, 115, 116, 113 
stringSpeed2(SIZE = 100000): 255, 122, 123, 123, 121 
stringSpeed3(SIZE = 100000): 257, 123, 129, 124, 125 

stringSpeed1(SIZE = 10000): 113, 25, 14, 13, 13 
stringSpeed2(SIZE = 10000): 118, 23, 24, 16, 14 
stringSpeed3(SIZE = 10000): 120, 24, 16, 17, 14 

//This run SIZE is very interesting. 
stringSpeed1(SIZE = 1000): 55, 22, 8, 6, 4 
stringSpeed2(SIZE = 1000): 54, 23, 7, 4, 3 
stringSpeed3(SIZE = 1000): 58, 23, 7, 4, 4 

stringSpeed1(SIZE = 100): 6, 6, 6, 6, 6 
stringSpeed2(SIZE = 100): 6, 6, 5, 6, 6 
stirngSpeed3(SIZE = 100): 8, 6, 7, 6, 6 

Wie Sie aus meinen Ergebnissen, auf Werte sehen, die in den „mittleren Bereich“ jedes Mal in Folge rep schneller bekommt sind. Dies wird, glaube ich, dadurch erklärt, dass die JVM rennt und sich den Speicher aneignet, den sie benötigt. Wenn die "Größe" ansteigt, darf dieser Effekt nicht mehr übernommen werden, da zu viel Speicher für den Garbage Collector vorhanden ist und der Prozess sich wieder einklinken kann. Wenn Sie einen "repetitiven" Benchmark wie diesen verwenden, bei dem der Großteil Ihres Prozesses in niedrigeren Cache-Ebenen statt im RAM-Speicher vorhanden sein kann, ist Ihr Prozess für Branch-Prädiktoren noch empfindlicher. Diese sind sehr schlau und würden herausfinden, was Ihr Prozess macht, und ich kann mir vorstellen, dass die JVM dies verstärkt. Dies hilft auch zu erklären, warum die Werte in anfänglichen Schleifen langsamer sind und warum die Art, wie Sie sich dem Benchmarking nähern, eine schlechte Lösung ist. Deshalb denke ich, dass Ihre Ergebnisse für Werte, die nicht "groß" sind, verzerrt sind und seltsam erscheinen. Wenn der "memory footprint" Ihres Benchmarks erhöht wird, wirkt sich diese Verzweigungsvorhersage weniger (prozentual) aus als die großen Strings, die Sie angehängt haben, im RAM verschoben werden.

Vereinfachte Schlussfolgerung: Ihre Ergebnisse für "große" Läufe sind einigermaßen gültig und scheinen mir ähnlich zu sein (obwohl ich immer noch nicht vollständig verstehe, wie Sie Ihre Ergebnisse erhalten haben, aber die Prozentsätze scheinen im Vergleich gut zu sein). Ihre Ergebnisse für kleinere Läufe sind jedoch aufgrund der Art Ihres Tests nicht gültig.

+0

Ich denke, du bist auf etwas mit den Branch Prädiktoren, ich dachte, es war die GC die kleineren Läufe verzerren, aber ich liebe deinen Drop von Lauf 1,2 bis 3 ... das sagt mir Volumes. Wie für die größeren Läufe, zeigen Sie den gleichen Effekt, die Methoden sind nicht gleichwertig – greedybuddha

+0

Sie postulieren hier viele Annahmen, von denen die meisten definitiv falsch sind. Der offensichtlichste Grund für die Beschleunigung niedrigerer Batchgrößen ist, dass die JVM mit der Interpretation des Bytecodes beginnt, nach einigen Durchläufen wird der Bytecode in nativen Maschinencode kompiliert (was eine einmalige Strafe verursacht, aber eine erhebliche Beschleunigung bei der nachfolgenden) runs) und nach weiteren Läufen kann die JVM sogar den Byte-Code unter Verwendung verschiedener Optimierungsstrategien auf der Grundlage von gesammelten Laufzeitstatistiken mehrmals neu kompilieren. – jarnbjo

+0

Auch ich bin mir nicht sicher, was Sie meinen, wie ich meine Ergebnisse bekam. Ich habe nur die Zeiten für jede Wiederholung summiert, ähnlich wie du es gemacht hast, aber mit + =, dann dividiert durch die Anzahl der Wiederholungen am Ende. – greedybuddha