2013-03-13 10 views
9

Czytałem o tym, jak w miarę możliwości kompilator java będzie kompilował ciągi połączone z operatorem "+" w instancje StringBuilder, i jak to czyni lepiej korzystać z prostego operatora "+", ponieważ są one compile to the same code. (Z wyjątkiem sytuacji, gdy budujesz ciąg w pętli while, w tym przypadku najlepiej jest użyć StringBuilder.)StringBuilder vs. .concat vs. "+" Różni się względna wydajność operatora w zaćmieniu niż w linii poleceń?

Przeczytałem również, że metoda .concat w łańcuchach jest najgorsza choice all the time (tak bardzo że został zrobiony w błąd przez Findbugs!).

Postanowiłem przetestować to sam, pisząc małą klasę Java w czasie zaćmienia. Moje wyniki zaskoczyły mnie nieco. Odkryłem, że różne metody były względnie szybsze lub wolniejsze, jeśli zastosowałem je i uruchomiłem w zaćmieniu, a nie w linii poleceń.

Pierwsze moje wyniki były eclipse:

the total millis to concatenate with + was: 12154 
the total millis to concatenate with .concat was: 8840 
the total millis to concatenate with StringBuilder was: 11350 
the total millis to concatenate with StringBuilder with a specified size was: 5611 

Więc w Eclipse StringBuilder z wielkością określoną był najszybszy, a następnie .concat (dziwne), a następnie StringBuilder i "+" konkatenacji były prawie takie same.

Moje wyniki w wierszu poleceń, jednak były:

the total millis to concatenate with + was: 4139 
the total millis to concatenate with .concat was: 8590 
the total millis to concatenate with StringBuilder was: 10888 
the total millis to concatenate with StringBuilder with a specified size was: 6033 

Więc kiedy skompilowane i wybiegł z commnad linii operator „+” był wyraźnie najszybszy, a następnie przez konstruktora String z rozmiaru, a następnie concat, a ostatnio był normalny StringBuilder!

To nie ma dla mnie sensu. Oczywiście wszystkie odpowiedzi na stackoverflow czytałem mówiąc, że operatory + kompilacji do normalnych starych instancji StringBuilder muszą być nieaktualne.

Czy ktoś wie, co się tutaj naprawdę dzieje?

Używam jdk1.7.0_07, i o ile mogę powiedzieć zarówno zaćmienie i mojej linii poleceń odnoszą się dokładnie do tego samego. Jedyną różnicą, jaką znam, jest to, że Eclipse używa "javaw", ale z tego, co przeczytałem, nie powinno to mieć znaczenia.

Oto moja klasa testowa, jeśli chcesz sprawdzić, czy nie robię niczego złego, ale jestem prawie pewien, że jest solidny.

public class Test { 

    static final int LOOPS = 100000000; 
    static final String FIRST_STRING = "This is such"; 
    static final String SECOND_STRING = " an awesomely cool "; 
    static final String THIRD_STRING = "to write string."; 

    /** 
    * @param args 
    */ 
    public static void main(String[] args) { 

     Test.plusOperator(); 
     Test.dotConcat(); 
     Test.stringBuilder(); 
     Test.stringBuilderSizeSpecified(); 

    } 

    public static void plusOperator() { 
     String localOne = FIRST_STRING; 
     String localTwo = SECOND_STRING; 
     String localThree = THIRD_STRING; 

     Calendar startTime = Calendar.getInstance(); 
     for (int x = 0; x < LOOPS; x++) { 
      String toPrint = localOne + localTwo + localThree; 
     } 
     Calendar endTime = Calendar.getInstance(); 
     System.out.println("the total millis to concatenate with + was: " + 
       (endTime.getTimeInMillis() - startTime.getTimeInMillis())); 
    } 

    public static void stringBuilder() { 
     String localOne = FIRST_STRING; 
     String localTwo = SECOND_STRING; 
     String localThree = THIRD_STRING; 

     Calendar startTime = Calendar.getInstance(); 
     for (int x = 0; x < LOOPS; x++) { 
      StringBuilder toBuild = new StringBuilder() 
       .append(localOne) 
       .append(localTwo) 
       .append(localThree); 
     } 
     Calendar endTime = Calendar.getInstance(); 
     System.out.println("the total millis to concatenate with StringBuilder was: " + 
       (endTime.getTimeInMillis() - startTime.getTimeInMillis())); 
    } 

    public static void stringBuilderSizeSpecified() { 
     String localOne = FIRST_STRING; 
     String localTwo = SECOND_STRING; 
     String localThree = THIRD_STRING; 

     Calendar startTime = Calendar.getInstance(); 
     for (int x = 0; x < LOOPS; x++) { 
      StringBuilder toBuild = new StringBuilder(50) 
       .append(localOne) 
       .append(localTwo) 
       .append(localThree); 
     } 
     Calendar endTime = Calendar.getInstance(); 
     System.out.println("the total millis to concatenate with StringBuilder with a specified size was: " + 
       (endTime.getTimeInMillis() - startTime.getTimeInMillis())); 
    } 

    public static void dotConcat() { 
     String localOne = FIRST_STRING; 
     String localTwo = SECOND_STRING; 
     String localThree = THIRD_STRING; 

     Calendar startTime = Calendar.getInstance(); 
     for (int x = 0; x < LOOPS; x++) { 
      String toPrint = localOne.concat(localTwo).concat(localThree); 
     } 
     Calendar endTime = Calendar.getInstance(); 
     System.out.println("the total millis to concatenate with .concat was: " + 
       (endTime.getTimeInMillis() - startTime.getTimeInMillis())); 
    } 

} 
+3

Przydałoby się poznać szczegóły używanej maszyny JVM. –

+9

Twoja metodologia testów porównawczych jest wyjątkowo podejrzana. Nie pozwala to na rozgrzanie się JIT; używa 'Calendar' zamiast wyznaczonej różnicy czasu bezwzględnego' System.nanoTime() '... –

+0

Uruchomiłem to wiele razy różnymi metodami wywoływanymi w różnych zamówieniach. Może to zrobić około 100 milisekund, ale mówimy tu o tysiącach różnicy milisekund, więc nie widzę, aby różnica między System.nanoTime() lub JIT rozgrzewała się. – CorayThan

Odpowiedz

12

Na Oracle JDK 1.7 (javac 1.7.0_17), operator "+" jest nadal realizowany za pomocą StringBuilder, jak pokazano uruchamiając javap -c na klasy, aby uzyskać kodu bajtowego (tylko pokazując pętle tutaj):

public static void plusOperator(); 
Code: 

    16: iload   4 
    18: ldc   #10     // int 100000000 
    20: if_icmpge  53 
    23: new   #11     // class java/lang/StringBuilder 
    26: dup   
    27: invokespecial #12     // Method java/lang/StringBuilder."<init>":()V 
    30: aload_0  
    31: invokevirtual #13     // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 
    34: aload_1  
    35: invokevirtual #13     // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 
    38: aload_2  
    39: invokevirtual #13     // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 
    42: invokevirtual #14     // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 
    45: astore  5 
    47: iinc   4, 1 
    50: goto   16 


public static void stringBuilder(); 
Code: 

    16: iload   4 
    18: ldc   #10     // int 100000000 
    20: if_icmpge  50 
    23: new   #11     // class java/lang/StringBuilder 
    26: dup   
    27: invokespecial #12     // Method java/lang/StringBuilder."<init>":()V 
    30: aload_0  
    31: invokevirtual #13     // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 
    34: aload_1  
    35: invokevirtual #13     // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 
    38: aload_2  
    39: invokevirtual #13     // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 
    42: astore  5 
    44: iinc   4, 1 
    47: goto   16 

Jedyna różnica między tymi dwoma polega na tym, że wersja z "+" konwertuje StringBuilder na String w pętli.

Pojawia się zatem pytanie: dlaczego test wykazuje tak różne wyniki dla tego samego kodu. Lub bardziej kompletnie, , dlaczego nie jest to prawidłowy mikro-benchmark.Oto kilka możliwych przyczyn:

  • Liczysz czas na ścianie. Oznacza to, że faktycznie mierzysz wszystko, co robi maszyna JVM podczas wykonywania testu. Co obejmuje wyrzucanie śmieci (co jest ważne, ponieważ tworzysz dużo śmieci). Możesz to złagodzić, pobierając czas procesora wątku.
  • Nie sprawdzasz, czy HotSpot kompiluje metody. Dlatego powinieneś zrobić fazę rozgrzewki przed jakimkolwiek mikro-benchmarkami: po prostu uruchom swój main() wiele razy, zanim wykonasz właściwy test.
+0

I, oczywiście, metoda toString() robi różnicę: musi utworzyć nową tablicę znaków, skopiować znaki z tablicy char konstruktora do tablicy char łańcucha, skonstruować nowy obiekt String i usunąć go ze śmieci . –

+0

Biorąc pod uwagę twój post i komentarz MrSmith, zgadzam się, że mój mikro-benchmark był nieważny. Nadal uważam, że to dziwne, że bierze zagnieżdżone pętle tysiące razy, aby "ustabilizować" wyniki. Jeśli zrobię rozgrzewkę, czy to czyni bardziej realistycznym punktem odniesienia? Czy ciąg konkatenacji w, powiedzmy, prawdziwej aplikacji internetowej być zoptymalizowany w ten sposób, czy może być bardziej podobny do tego, jak działa pre-optymalizacja przez długi czas. W końcu to nie ma znaczenia - i tak jest to bardziej teoretyczne niż praktyczne, ale wydają mi się interesujące pytania. – CorayThan

+0

Brian Goetz napisał artykuł o mikro-benchmarkingu kilka lat temu. Nie mam linku pod ręką, ale powinien szybko pojawić się w Google. – parsifal

1

spróbować umieścić StringBuilder toBuild = new StringBuilder() powyżej pętli. To samo z String toPrint i zrobić += dla napisu, a zobaczysz różnicę.
Nie twórz nowego ciągu i StringBuilder w pętli.

+1

Testuję wydajność pojedynczej linii różnych metod łączenia. Wiem, że konstruktor String byłby DUŻO lepszy, gdybym testował wydajność jednego łańcucha tworzonego w pętli. Celem pętli jest uśrednienie losowych różnic w konkatenacji pojedynczej linii. Po prostu myślę, że to dziwne, że operator + wydaje się lepszy od StringBuilder'a w przypadku konkatenacji ciągów w linii, kiedy przeczytałem, że operator + kompiluje się jako StringBuilder. – CorayThan