Mam pytanie związane z wydajnością dotyczące używania StringBuilder. W bardzo długiej pętli manipuluję a StringBuilder
i przekazuję go do innej metody, takiej jak ta:
for (loop condition) {
StringBuilder sb = new StringBuilder();
sb.append("some string");
. . .
sb.append(anotherString);
. . .
passToMethod(sb.toString());
}
Czy tworzenie instancji StringBuilder
w każdym cyklu pętli jest dobrym rozwiązaniem? I czy zamiast tego wywołanie usuwania jest lepsze, jak poniżej?
StringBuilder sb = new StringBuilder();
for (loop condition) {
sb.delete(0, sb.length);
sb.append("some string");
. . .
sb.append(anotherString);
. . .
passToMethod(sb.toString());
}
java
performance
string
stringbuilder
Pier Luigi
źródło
źródło
W filozofii pisania solidnego kodu zawsze lepiej jest umieścić StringBuilder w pętli. W ten sposób nie wychodzi poza kod, dla którego jest przeznaczony.
Po drugie, największe ulepszenie w StringBuilder polega na nadaniu mu początkowego rozmiaru, aby uniknąć jego powiększania się, gdy pętla działa
źródło
Jeszcze szybciej:
W filozofii pisania solidnego kodu, wewnętrzne działanie metody powinno być ukryte przed obiektami, które ją używają. Dlatego z punktu widzenia systemu nie ma różnicy, czy ponownie zadeklarujesz StringBuilder w pętli, czy poza pętlą. Ponieważ zadeklarowanie go poza pętlą jest szybsze i nie sprawia, że kod jest bardziej skomplikowany do odczytania, należy ponownie użyć obiektu zamiast go odtworzyć.
Nawet jeśli kod był bardziej skomplikowany i wiesz na pewno, że wąskim gardłem była instancja obiektu, skomentuj go.
Trzy przebiegi z tą odpowiedzią:
Trzy przebiegi z drugą odpowiedzią:
Chociaż nie jest to istotne, ustawienie
StringBuilder
początkowego rozmiaru bufora da niewielki zysk.źródło
Okej, teraz rozumiem, co się dzieje i ma to sens.
Miałem wrażenie, że
toString
właśnie przekazałem element bazowychar[]
do konstruktora String, który nie pobrał kopii. Kopia byłaby wtedy wykonywana przy następnej operacji „zapisu” (npdelete
.). Wydaje mi się, że tak było w przypadkuStringBuffer
niektórych poprzednich wersji. (Teraz nie jest). Ale nie -toString
po prostu przekazuje tablicę (wraz z indeksem i długością) do publicznej wiadomościString
konstruktora, który pobiera kopię.Tak więc w przypadku „ponownego wykorzystania
StringBuilder
” naprawdę tworzymy jedną kopię danych na łańcuch, używając przez cały czas tej samej tablicy znaków w buforze. Oczywiście tworzenie nowego zaStringBuilder
każdym razem tworzy nowy podstawowy bufor - a następnie ten bufor jest kopiowany (nieco bezcelowo, w naszym konkretnym przypadku, ale robione ze względów bezpieczeństwa) podczas tworzenia nowego ciągu.Wszystko to prowadzi do tego, że druga wersja jest zdecydowanie bardziej wydajna - ale jednocześnie powiedziałbym, że jest to brzydszy kod.
źródło
Ponieważ wydaje mi się, że nie zostało to jeszcze wskazane, z powodu optymalizacji wbudowanych w kompilator Sun Java, który automatycznie tworzy StringBuilders (StringBuffers przed J2SE 5.0), gdy widzi konkatenacje ciągów, pierwszy przykład w pytaniu jest równoważny:
Co jest bardziej czytelne, IMO, lepsze podejście. Twoje próby optymalizacji mogą przynieść zyski na jednej platformie, ale potencjalnie stracić inne.
Ale jeśli naprawdę masz problemy z wydajnością, na pewno zoptymalizuj. Zacząłbym jednak od jawnego określenia rozmiaru buforu StringBuilder, według Jona Skeeta.
źródło
Współczesna maszyna JVM jest naprawdę sprytna w takich sprawach. Nie odgadłbym tego po raz drugi i zrobiłbym coś hakerskiego, co jest trudniejsze w utrzymaniu / czytelności ... chyba że wykonasz odpowiednie testy porównawcze z danymi produkcyjnymi, które potwierdzą nietrywialną poprawę wydajności (i udokumentujesz to;)
źródło
Opierając się na moim doświadczeniu w tworzeniu oprogramowania w systemie Windows, powiedziałbym, że wyczyszczenie StringBuilder podczas pętli ma lepszą wydajność niż tworzenie instancji StringBuilder z każdą iteracją. Wyczyszczenie go zwalnia tę pamięć do natychmiastowego nadpisania bez konieczności dodatkowego przydziału. Nie jestem wystarczająco zaznajomiony z kolektorem śmieci w Javie, ale myślę, że zwalnianie i brak ponownej alokacji (chyba że twój następny ciąg powiększa StringBuilder) jest bardziej korzystne niż tworzenie instancji.
(Moja opinia jest sprzeczna z tym, co sugerują wszyscy inni. Hmm. Czas to porównać.)
źródło
Powodem, dla którego wykonanie „setLength” lub „delete” poprawia wydajność, jest głównie to, że kod „uczy się” odpowiedniego rozmiaru bufora, a mniej przy alokacji pamięci. Generalnie zalecam zezwolenie kompilatorowi na optymalizację ciągów . Jeśli jednak wydajność jest krytyczna, często wstępnie obliczam oczekiwany rozmiar bufora. Domyślny rozmiar StringBuilder to 16 znaków. Jeśli przekroczysz to, musisz zmienić rozmiar. Zmiana rozmiaru jest przyczyną utraty wydajności. Oto kolejny mini-test, który to ilustruje:
Wyniki pokazują, że ponowne użycie obiektu jest około 10% szybsze niż utworzenie bufora o oczekiwanym rozmiarze.
źródło
LOL, pierwszy raz widziałem ludzi porównujących wydajność, łącząc string w StringBuilder. W tym celu, jeśli użyjesz „+”, może to być jeszcze szybsze; D. Celem użycia StringBuilder do przyspieszenia pobierania całego ciągu jako koncepcji „lokalności”.
W scenariuszu, w którym często pobierasz wartość String, która nie wymaga częstych zmian, Stringbuilder umożliwia wyższą wydajność pobierania ciągów. I to jest cel używania Stringbuildera ... proszę nie testować MIS podstawowego celu tego ...
Niektórzy mówili, że samolot leci szybciej. Dlatego przetestowałem to na moim rowerze i stwierdziłem, że samolot porusza się wolniej. Czy wiesz, jak ustawiam ustawienia eksperymentu; D
źródło
Nie znacznie szybciej, ale z moich testów wynika, że jest średnio o kilka milisek szybszy przy użyciu 1.6.0_45 64 bity: użyj StringBuilder.setLength (0) zamiast StringBuilder.delete ():
źródło
Najszybszym sposobem jest użycie „setLength”. Nie będzie wymagać operacji kopiowania. Sposób tworzenia nowego StringBuildera powinien być całkowicie wyłączony . Powolne działanie StringBuilder.delete (int start, int end) jest spowodowane tym, że ponownie skopiuje tablicę dla części zmieniającej rozmiar.
Następnie StringBuilder.delete () zaktualizuje StringBuilder.count do nowego rozmiaru. Chociaż StringBuilder.setLength () po prostu upraszcza aktualizację StringBuilder.count do nowego rozmiaru.
źródło
Pierwsza jest lepsza dla ludzi. Jeśli druga jest nieco szybsza w niektórych wersjach niektórych maszyn JVM, to co z tego?
Jeśli wydajność jest tak krytyczna, pomiń StringBuilder i napisz własne. Jeśli jesteś dobrym programistą i weźmiesz pod uwagę sposób, w jaki Twoja aplikacja korzysta z tej funkcji, powinieneś być w stanie zrobić to jeszcze szybciej. Wart? Prawdopodobnie nie.
Dlaczego to pytanie jest traktowane jako „ulubione pytanie”? Ponieważ optymalizacja wydajności to świetna zabawa, bez względu na to, czy jest praktyczna, czy nie.
źródło
Nie wydaje mi się, aby próba optymalizacji wydajności w ten sposób miała sens. Dzisiaj (2019) obie statystyki działają przez około 11 sekund dla pętli 100.000.000 na moim laptopie I5:
==> 11000 ms (deklaracja wewnątrz pętli) i 8236 ms (deklaracja poza pętlą)
Nawet jeśli uruchamiam programy do dedykowania adresów z kilkoma miliardami pętli, różnica wynosi 2 sekundy. dla 100 milionów pętli nie robi żadnej różnicy, ponieważ te programy działają godzinami. Pamiętaj również, że sytuacja wygląda inaczej, jeśli masz tylko jedną instrukcję append:
==> 3416 ms (pętla wewnętrzna), 3555 ms (pętla zewnętrzna) Pierwsza instrukcja, która tworzy StringBuilder w pętli, jest w tym przypadku szybsza. A jeśli zmienisz kolejność wykonywania, jest to znacznie szybsze:
==> 3638 ms (pętla zewnętrzna), 2908 ms (pętla wewnętrzna)
Pozdrawiam, Ulrich
źródło
Zadeklaruj raz i przydziel za każdym razem. Jest to koncepcja bardziej pragmatyczna i nadająca się do ponownego wykorzystania niż optymalizacja.
źródło