Stringi są na ciekawym tworem – zbyt rozbudowane, aby dało się je zamknąć w typ prymitywny, a jednocześnie operacje na nich muszą być bardzo wydajne.
Jedną z podstawowych operacji na nich jest tworzenie nowych łańcuchów znaków poprzez łączenie różnych zmiennych. Jednym słowem – konkatenacja. Można ją uzyskać na wiele różnych sposobów m. in. operator +, StringBuilder, StringBuffer lub String.format().
W tym artykule opowiem nieco o operatorze +.
Implementacja
Załóżmy, iż interesuje nas następująca operacja.
public String helloWorldFromOldMen(long age) { return "Hello world from " + age + " years old men"; }Warto na wstępie zajrzeć, co mówi oficjalna dokumentacja Java Language Specification o konkatenacji.
An implementation may choose to perform conversion and concatenation in one step to avoid creating and then discarding an intermediate String object. To increase the performance of repeated string concatenation, a Java compiler may use the StringBuffer class or a similar technique to reduce the number of intermediate String objects that are created by evaluation of an expression.
The Java® Language Specification, Java SE 15 Edition
Co ciekawe, ta część jest niezmienna od specyfikacji dla Javy 1.0.
I rzeczywiście, jeżeli skompilujemy kod kompilatorem do Javy 1.4, to rezultat (po skompilowaniu i dekompilacji) będzie następujący:
Code: stack=3, locals=3, args_size=2 0: new #2 // class java/lang/StringBuffer 3: dup 4: invokespecial #3 // Method java/lang/StringBuffer."":()V 7: ldc #4 // String Hello world from 9: invokevirtual #5 // Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer; 12: lload_1 13: invokevirtual #6 // Method java/lang/StringBuffer.append:(J)Ljava/lang/StringBuffer; 16: ldc #7 // String years old men 18: invokevirtual #5 // Method java/lang/StringBuffer.append:(Ljava/lang/String;)Ljava/lang/StringBuffer; 21: invokevirtual #8 // Method java/lang/StringBuffer.toString:()Ljava/lang/String; 24: areturnJednak StringBuffer ma tę adekwatność, iż jego metody są synchronizowane. W przypadku konkatenacji stringów, wykonywanych w tym samym wątku, jest to zbędne. Z tego powodu w Javie 1.5 zdecydowano zamienić StringBuffer na StringBuilder, który tej synchronizacji jest pozbawiony.
Kolejnym krok nastąpił w Javie 1.6 – wprowadzono usprawnienie, pozwalające JITowi C2 zamienić użycia StringBuilder na tworzenie Stringa bez konieczności tworzenia obiektu StringBuilder. W Javie 1.7 włączono ten mechanizm domyślnie. Okazało się, jednak, iż ta opcja (OptimizeStringConcat) jest „krucha” (ang. fragile) i sprawia problemy przy dalszej optymalizacji Stringa.
JEP-280
Postanowiono zastosować ten sam mechanizm, co przy implementacji Lambd. Zamiast na etapie kompilacji ustalać, jak jest wykonywana konkatenacja Stringów, pozwólmy na wygenerowanie tego kodu przez JVMa przed pierwszym uruchomieniem.
Takie podejście pozwala na eliminowanie wstecznej kompatybilności zastosowanych optymalizacji, gdzie zmiany w starszej Javie musiały również działać w nowszej. Jednocześnie kod skompilowany w starszej Javie po uruchomieniu na nowszym JVMie automatycznie działał szybciej, gdyż optymalizacje robione są przy pierwszym uruchomieniu.
Wspomniany JEP-280 został wdrożony w Javie 9.
A jak to będzie w wydajności?
Generalnie – szybciej.
Przy generowaniu kodu konkatenacji aktualnie jest dostępnych 6 strategii, przy czym domyślnie włączona jest najefektywniejsza. Pozwala ona na konkatenowanie 3-4 krotnie szybciej, jednocześnie wykorzystując 3-4 razy mniej pamięci (w ekstremalnych przypadkach 6.36x szybciej i 6.36x mniej pamięci). Tworzenie Stringów w tej strategii odbywa się praktycznie bez tworzenia dodatkowych obiektów, po których GC musiałby sprzątać.
Jednokrotny narzut wynikający z konieczności wygenerowania kodu w Runtime’ie jest stosunkowo mały – do 30ms.
Podsumowanie
Szczerze mówiąc, tkwiło we mnie przekonanie, iż jak konkatenacja Stringów to tylko i wyłącznie StringBuilder, bo inaczej jest „nieefektywnie”. Okazuje się jednak, iż operator + może być bardziej efektywny w prostych przypadkach.
Kolejny raz można powiedzieć, iż jeżeli chcesz pomóc JVMowi w optymalizacji, to pisz porządny, czytelny, rzemieślniczy kod.
Jeśli chodzi o linki do poczytania, to:
Pax et bonum