Cokolwiek kodujemy, konieczność zbudowania dużego łańcucha znaków składającego się z kolejno dołączanych fragmentów pojawia się bardzo często. To może być zapytanie do bazy danych, misternie wyrzeźbiony adres URL, dynamicznie generowany i kompilowany później shader czy w końcu obszerny i szczegółowy komunikat o błędzie.
Wspólną cechą tych tekstów jest to, że budujemy je stopniowo, kawałek po kawałku. Na takie okazje niektóre języki (jak .NET-owe lub Java) posiadają specjalne klasy w rodzaju StringBuilder
. Mają one być efektywniejsze niż bezpośrednie używanie typów string
, głównie ze względu na nietworzenie wielu łańcuchów i niezaśmiecanie nimi pamięci, co odciąża garbage collector z dodatkowej pracy.
W C++ nie ma oczywiście odśmiecacza, który musiałby po nas sprzątać, i nie ma też narzędzi typu StringBuilder
. Czy to oznacza więc, że możemy radośnie korzystać z samej klasy std::string
do budowania dowolnie długich tekstów, bo żadnej efektywniejszej alternatywy nie ma?… To właśnie zdecydowałem się sprawdzić, przeprowadzając mały eksperyment z tworzeniem długich łańcuchów znaków kilkoma sposobami, aby potomni nie musieli rozwiązywać tego jakże uciążliwego dylematu ;]
Przechodząc do rzeczy, wpierw wysmażyłem taką oto funkcję która generuje tekst o podanej minimalnej długości:
Wyróżnienie dwóch sposobów polegało natomiast na tym, że w jednym z nich dodatkowo wywoływałem na samym początku metodę reserve
w celu uprzedniej rezerwacji pamięci w wektorze na 3 * minLen / 2
elementów.
Testy przeprowadziłem na standardowej implementacji klas string
i vector
dostępnej w Visual C++ 2005. Wyniki eksperymentu przedstawiają się następująco:
Można w nich przede wszystkim zauważyć, że zarządzanie pamięcią w klasie string
jest domyślnie znacząco lepsze niż w vector
. Najszybszą metodą konstruowania napisów okazuje się jednak użycie wektora z uprzednią rezerwacją pamięci, która okazuje się o ok. 20% szybsza niż zwykła konkatenacja string
ów.
Czy to oznacza, że powinniśmy stworzyć sobie własną klasę StringBuilder
, opierającą się na wektorze właśnie, i jej używać? Niekoniecznie. Jak widać, znaczące różnice pojawiają się dopiero przy tworzeniu napisów o wielkościach rzędu megabajta lub więcej. Konia z rzędem temu, kto tworzy tak duże zapytania lub shadery :) Do większości typowych zastosowań bezpośrednie użycie typu string
wydaje się więc wystarczające.
No chyba że istnieją bardziej efektywne sposoby na budowanie stringów w C++, których nie udało mi się wymyślić – co jest swoją drogą całkiem prawdopodobne :)
Wydaje mi się, że _jedynym_ powodem stosowania napisów modyfikowalnych (np. Javowski StringBuilder) w językach z garbage collectorami jest właśnie zapobieżenie śmietnika w pamięci. W językach (jak C++) w których samemu należy zwalniać pamięć, nigdy nie powstaje śmietnik w pamięci… czyli taki normalny string musi przy konkatenacji jakoś sobie radzić by nie naśmiecić (w Javie robi to dopiero StringBuilder). Właśnie brakiem możliwości odciążenia GC bym tłumaczył brak klas typu StringBuildera w językach bez GC.
Jak można wyczytać ze statystyk które zrobiłeś, faktycznie, tak jak myślałem, na szybkości nie wiele można tutaj zyskać. Względem stringa udało Ci się to dopiero alokując miejsce zawczasu – czyli poniekąd oszukując :D bo taki biedny string nie ma pojęcia ile możesz chcieć do niego zapisać. Proponowałbym zrobienie jeszcze jednej wersji, w której wykorzystany był by string, ale z rezerwacją miejsca (poprzez wywołanie string.resize(3 * minLen / 2) na początku).
Ja bym na samiuteńkim początku zrobiła string o wymaganym rozmiarze, a potem go tylko wypełniała…
Hmm. Czemu nie znalazl sie wsrod tych sposobow stringstream ? Czyzby z zalozenia slowko “stream” zakladalo jego powolnosc ( co w sumie jest calkiem prawdopodobne ) ;) ?
Poza tym wydaje mi sie ze mozliwosc prostych konwersji z roznych typow to plus dla tej metody przy roznych zapytaniach wlasnie.
A stringstream? Jak wypada na tle powyższych?
Zrobiłem na szybko testy dla dwóch zaproponowanych sposobów i wyniki są całkiem ciekawe ;]
stringstream jest absolutnie do kitu, z czasem działania dla dwóch ostatnich przypadków równym odpowiednio 175 i 338 ms. To akurat mało zaskakujące, zważywszy na to jak złożonym tworem są strumienie IOStreams.
Za to użycie klasy string z uprzednim wywołaniem reserve (bo chyba o to ci chodziło TeWu, a nie o resize? :)) daje dwa ostatnie wyniki rzędu 18 i 37 ms. Czyli zdecydowanie wygrywa :)
Tak więc w sumie w obu kategoriach (z oszacowaniem długości i bez niego) wygrywa klasa string. Wygląda zatem, że C++ rzeczywiście żadnego StringBuildera nie potrzebuje i że w językach go zawierających cała sprawa rozbija się o garbage collector.
Sprawdź jeszcze strstream. Wtedy test będzie kompletny.
Jeszcze tylko char* + memcpy i każdy znajdzie swojego faworyta. ;)
Chociaż dziwi mnie różnica między string+reserve i vector+reserve. W końcu co vector robi w resize(), gdy ma wymagany rozmiar? Zakładam, że nic.
char* + memcpy? Brzydko, lepiej char* + ofastream – niby strumień, a potrafi pokonać ręczne zarządzanie char*.
hmm…
http://www.msobczak.com/prog/fastreams/
Robisz dużo złączeń długich stringów, więc może rope z STL się nada?
http://www.sgi.com/tech/stl/Rope.html
Faktem jest, że ten test pokazuje jak bardzo nie opłaca się zbyt wcześnie optymalizować kodu :-)
fatal error C1083: Cannot open include file: ‘rope’: No such file or directory ;)
jeżeli chodzi o inne struktury, spróbowałbym jeszcze z STLową Listą. Wszakże gdy znamy przewidywalną długość stringa, to string z reserve znów powinien być szybszy, jednakże nie znając owej długości, powinniśmy się skupić na strukturze, która gwarantuje szybkie wykonanie operacji dodawania.
Takie budowanie składało by się kolejno z wywołań
dopiero na sam koniec używałbym stringa do zebrania wszystkiego w kupę, rezerwując wcześniej obszar o długości len