Dostałem reklamacje od kumpla, który spodziewał się jakichś benchmarków porównujących wydajność konkatenacji w kolejnych wersjach JVMów. Oczywiście chodzi o dopełnienie poprzedniego wpisu.
Co sprawdzamy i jak?
Założyłem, iż porównam działanie kodu skompilowane różnymi wersjami kompilatorów javac z JDK 1.4, 1.8 i 15. Następnie te skompiowane kody wrzucę do plików Jar, które będę uruchamiał na kolejnych wersjach JVMa.
Nie wiem, ilu z Was kompilowało kod z poziomu terminala. Osobiście preferuję skorzystanie z Mavena, ewentualnie z poziomu IDE. Jednak w ramach ćwiczenia stwierdziłem, iż skompiluję i spakuję do Jar z użyciem linii komend. Dla jednego pliku nie okazało się to zbyt trudne
javac src/main/java/dev/jgardo/jvm/miscellaneous/string/StringConcatenation.java jar cf ./string.jar dev/jgardo/jvm/miscellaneous/string/StringConcatenation.classNastępnie uruchamiałem następujące benchmarki w JVMek różnych wersji (1.7,1.8,11,15 – wszystkie z rodziny OpenJDK), które korzystają z bibliotek skompilowanych różnymi wersjami javac.
private static final StringConcatenation CONCATENATION = new StringConcatenation(); @Benchmark public String concatenation() { return CONCATENATION.helloWorldFromOldMen(12); }Gdzie StringConcatenation to:
public class StringConcatenation { public String helloWorldFromOldMen(long age) { return "Hello " + " world from " + age + " years old men"; } }Wyniki
JVM \ jar | 1.4 [ns/op] | 1.7/1.8 [ns/op] | >9 [ns/op] |
OpenJDK 1.7 | 101,435 ą 2,746 | 66,310 ą 1,106 | – |
OpenJDK 1.8 | 98,539 ą 1,757 | 68,302 ą 1,228 | – |
OpenJDK 11 | 96,123 ą 1,137 | 54,094 ą 2,117 | 23,195 ą 0,172 |
OpenJDK 15 | 83,235 ą 1,837 | 55,243 ą 2,067 | 23,516 ą 0,301 |
Wyniki w zależności od wersji maszyny wirtualnej oraz wersji kompilatora javac. Wyniki wyrażone w ns/op.
Z tych wyników można wyciągnąć kilka wniosków:
- StringBuffer jest wolniejszy od StringBuildera
Mało odkrywcze – dodatkowa synchronizacja zawsze coś będzie kosztować. Jakkolwiek wydaje się, iż w tych nowszych JVMkach StringBuffer przyspieszył zawsze jest przynajmniej 1/3 wolniejszy niż StringBuilder - Uruchomienie konkatenacji skompilowanej javac w wersji 1.8 jest szybsze na OpenJDK 11 o około 20% niż na OpenJDK 1.8.
To w prawdopodobnie wynika z tego, iż w Java 9 zaczęto używać 1 bajtowego byte zamiast 2 bajtowego char. Więcej o tym choćby tutaj – JEP-254. - Uruchomienie konkatenacji skompilowanej javac w wersji 9 wzwyż powoduje skrócenie czasu o ok. 55%.
O tym efekcie wspominałem już w poprzednim wpisie. Notatka eksperymentalna zawierała prawdę
Pamięć
Zmierzyłem również ilość potrzebnej pamięci do wykonania konkatenacji. Również nie było to trudne – wystarczyło do benchmarku dodać jedną linijkę podpinającą GCProfiler. Wyniki w poniższej tabelce.
JVM \ jar | 1.4 [B/op] | 1.7/1.8[B/op] | >9[B/op] |
OpenJDK 1.7 | 272,000 ą 0,001 | 272,000 ą 0,001 | – |
OpenJDK 1.8 | 272,000 ą 0,001 | 272,000 ą 0,001 | – |
OpenJDK 11 | 200,000 ą 0,001 | 168,000 ą 0,001 | 80,000 ą 0,001 |
OpenJDK 15 | 200,028 ą 0,002 | 168,019 ą 0,001 | 80,009 ą 0,001 |
Wyniki w zależności od wersji maszyny wirtualnej oraz wersji kompilatora javac. Wyniki wyrażone w B/op.
Również i tutaj jestem winien kilku słów komentarza:
- StringBuilder i StringBuffer uruchomione na OpenJDK w wersji 9 wzwyż korzystają z wspomnianego wcześniej ulepszenia – JEP-254. Stąd redukcja o 25% zużycia pamięci względem uruchomienia na wersji 1.7 lub 1.8.
- Użycie konkatenacji skompilowanej javac w wersji 9 wzwyż pozwala na redukcję zużycia pamięci o 50% w porównaniu do konkatenacji skompilowanej javac w wersji 1.8 i o 67% w porównaniu do wersji 1.4.
Podsumowanie
Warto używać nowszej wersji Javy niż owiana sławą 1.8. Wbrew pozorom w nowych wersjach Javy wchodzą nie tylko nowe feature’y, ale i ulepszenia wydajności.
O ile konkatenacja w Javach 9+ jest znacznie bardziej wydajna, to rzadko kiedy jest to na tyle kluczowe, by zastąpić czytelne String.format, Logger.info itd. Czytelność jest ważna, a wydajność konkatenacji stringów może mieć marginalne znaczenie, jeżeli macie znacznie cięższe operacje do wykonania – operacje na bazie danych lub uderzenie HTTP na zewnętrzny serwis.
Warto też spojrzeć na minimalną wersję Javy wymaganą przez biblioteki jako na potencjalną możliwość przyspieszenia działania, zamiast wyłącznie na ograniczenie.
Pax