„Hello ” + name + „.”, czyli konkatenacja Stringów 0 (0)

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, że 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śli 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: areturn

Jednak StringBuffer ma tę właściwość, że 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, że 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, że jak konkatenacja Stringów to tylko i wyłącznie StringBuilder, bo inaczej jest „nieefektywnie”. Okazuje się jednak, że operator + może być bardziej efektywny w prostych przypadkach.

Kolejny raz można powiedzieć, że jeśli 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 🙂