2013-05-23 33 views
8

Tak więc byłem lead to believe, który używając operatora "+" do dołączenia Strings na jednej linii był tak samo wydajny jak użycie StringBuilder (i zdecydowanie o wiele ładniej na oczach). Dzisiaj jednak miałem problemy z szybkością z Loggerem, który dołączał zmienne i łańcuchy, korzystał z operatora "+". Zrobiłem więc szybki test case i ku mojemu zdziwieniu odkryłem, że używanie StringBuilder było szybsze!Różnica prędkości dla pojedynczej linii Łączenie ciągów znaków

Podstawy są używane średnio 20 przebiegów dla każdej liczby załączeń, z 4 różnymi metodami (pokazane poniżej).

wyników, czasu (w sekundach),

 
               # 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 - 

Wykres procentowego Różnica z najszybszych algorytmu.

% Difference in String timings

Mam wyrejestrowany byte code, jest inny dla każdej metody porównywania znaków.

Oto, czego używam do metod, i można zobaczyć całą klasę testową here.

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); 
} 

Próbowałem teraz z floats, ints i stringami. Wszystkie wykazują mniej więcej taką samą różnicę czasu.

Pytania

  1. Operator „+” nie jest wyraźnie coraz tego samego kodu bajtowego, a czas jest bardzo różni się od optymalnego. Co daje?
  2. Zachowanie się algorytmów między 100 a 10000 liczbą załączeń jest dla mnie bardzo dziwne, więc czy ktoś ma jakieś wyjaśnienie?
+0

Zachowanie algorytmów betwen 100 i .... ??? – ChrisCM

+0

naprawiono, z jakiegoś powodu zostało przerwane. – greedybuddha

+2

Świetne pytanie, z badaniami i danymi, aby je utworzyć. +1 – syb0rg

Odpowiedz

3

Java Language Specification nie precyzuje, w jaki sposób odbywa się ciąg konkatenacji, ale wątpię, że kompilator nic nie robi, ale ekwiwalent:

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

Można użyć „javap -c ... ", aby zdekompilować plik klasy i zweryfikować to.

Jeśli zmierzysz znaczącą i powtarzającą się różnicę w czasie wykonywania pomiędzy metodami, wolałbym raczej założyć, że odśmiecacz działa w różnym czasie, niż że istnieje faktyczna, znacząca różnica wydajności. Utworzenie StringBuilder s o różnych początkowych pojemnościach może oczywiście mieć pewien wpływ, ale powinno być nieznaczne w porównaniu do wysiłku wymaganego np. Dla sformatuj pływaki.

+0

Dekompilowałem kod, a operator "+" jest inny niż StringBuilder(), chociaż nie wypróbowałem przeciwko StringBuilder ("[") ;, źle wyglądam na tej – greedybuddha

+0

Aktualizacja: kod bajtowy jest nawet inny niż nowy StringBuilder ("[") .append ... – greedybuddha

+0

Czy możesz dodać wynik javap do swojego pytania? – jarnbjo

4

Nie podobają mi się dwie rzeczy dotyczące twojego testu. Najpierw przeprowadziłeś wszystkie testy w ramach tego samego procesu. Kiedy mam do czynienia z "dużym" (niejednoznacznym, jaki znam), ale gdy zajmiesz się czymkolwiek, w czym twój proces wchodzi w interakcję z pamięcią, jest twoim głównym problemem, zawsze powinieneś testować w oddzielnym biegu. Sam fakt, że udało nam się zebrać śmieci, może mieć wpływ na wyniki z poprzednich serii. Sposób, w jaki uwzględniłeś wyniki, jest dla mnie niejasny. To, co zrobiłem, polegało na tym, że każdy z nich wykonywał poszczególne przebiegi i zerwał liczbę razy, gdy go prowadziłem. Pozwoliłem też uruchomić go dla kilku "powtórzeń", mierząc czas każdego powtórzenia. Następnie wydrukuj liczbę milisekund, które każdy z nich wykonał.Tu jest mój kodu:

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); 
    } 

} 

Teraz moje wyniki:

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 

Jak widać z moich wyników, na wartości, które są w „środkowych zakresach” każdy kolejny przedstawiciel dostaje szybciej. To, jak sądzę, jest wyjaśnione przez JVM, która zaczyna działać i chwyta pamięć, której potrzebuje. Wraz ze wzrostem "rozmiaru" ten efekt nie może zostać przejęty, ponieważ jest zbyt dużo pamięci, aby garbage collector mógł odpuścić, i aby proces znowu się zatrzasnął. Ponadto, gdy robisz "powtarzalny" test porównawczy w ten sposób, kiedy większość twojego procesu może istnieć na niższych poziomach pamięci podręcznej, a nie w pamięci RAM, twój proces jest jeszcze bardziej wyczulony na predyktory oddziału. Są bardzo sprytni i przywykli do tego, co robi twój proces, i wyobrażam sobie, że JVM to wzmacnia. Pomaga to również wyjaśnić, dlaczego wartości na początkowych pętlach są wolniejsze i dlaczego sposób, w jaki zbliżasz się do testu porównawczego, był kiepskim rozwiązaniem. Dlatego myślę, że twoje wyniki dla wartości, które nie są "duże", są zniekształcone i wydają się dziwne. Następnie, gdy "wskaźnik pamięci" twojego wzorca wzrósł, ta prognoza rozgałęzień ma mniejszy wpływ (procentowo) niż duże ciągi, do których dołączasz, przenoszone w RAM.

Uproszczona konkluzja: Twoje wyniki w przypadku "dużych" przebiegów są uzasadnione i wydają się podobne do moich (chociaż nadal nie rozumiem w pełni, jak uzyskałeś wyniki, ale wyniki wydają się dobrze zestawiać). Jednak wyniki dla mniejszych serii nie są ważne, ze względu na charakter testu.

+0

Myślę, że jesteś na czymś z predyktorami branży, pomyślałem, że to gc przekręca mniejsze przebiegi, ale bardzo podoba mi się twoja kropla z biegu 1,2 do 3 ... to mówi do mnie tom. Jeśli chodzi o większe przebiegi, dajesz ten sam efekt, metody nie są równoważne. – greedybuddha

+0

Postulujesz tutaj wiele założeń, z których większość jest ostatecznie błędna. Najbardziej oczywistym powodem przyspieszenia niższych partii jest to, że JVM rozpoczyna się od interpretacji kodu bajtowego, po kilku uruchomieniach kod bajtu jest kompilowany do natywnego kodu maszynowego (powodując jednorazową karę, ale znaczne przyspieszenie w kolejnych działa) i po kolejnych uruchomieniach maszyna JVM może nawet ponownie skompilować kod bajtowy kilka razy przy użyciu różnych strategii optymalizacji na podstawie zebranych statystyk czasu wykonywania. – jarnbjo

+0

Ponadto, nie jestem pewien, co masz na myśli, skoro mam wyniki. Właśnie podsumowałem czasy dla każdego powtórzenia, podobne do tego, co zrobiłeś, ale z + =, a następnie podzielone przez liczbę powtórzeń na końcu. – greedybuddha