Słyszałem, jak kilka osób martwiło się operatorem „+” w std :: string i różnymi obejściami, aby przyspieszyć konkatenację. Czy któreś z nich są naprawdę potrzebne? Jeśli tak, jaki jest najlepszy sposób łączenia ciągów w C ++?
108
Słyszałem, jak kilka osób martwiło się operatorem „+” w std :: string i różnymi obejściami, aby przyspieszyć konkatenację. Czy któreś z nich są naprawdę potrzebne? Jeśli tak, jaki jest najlepszy sposób łączenia ciągów w C ++?
libstdc++
robi to na przykład . Tak więc, wywołując operator + z tymczasowymi, może osiągnąć prawie równie dobrą wydajność - być może argument na korzyść tego, ze względu na czytelność, chyba że ma się testy porównawcze pokazujące, że jest to wąskie gardło. Jednak standardowa zmiennaappend()
byłaby zarówno optymalna, jak i czytelna ...Odpowiedzi:
Dodatkowa praca prawdopodobnie nie jest tego warta, chyba że naprawdę potrzebujesz wydajności. Prawdopodobnie uzyskasz znacznie lepszą wydajność, używając zamiast tego operatora + =.
Teraz, po tym zrzeczeniu się, odpowiem na twoje rzeczywiste pytanie ...
Wydajność klasy ciągu STL zależy od implementacji używanego pliku STL.
Możesz zagwarantować wydajność i mieć większą kontrolę , wykonując konkatenację ręcznie za pomocą wbudowanych funkcji c.
Dlaczego operator + nie jest wydajny:
Spójrz na ten interfejs:
Możesz zobaczyć, że nowy obiekt jest zwracany po każdym +. Oznacza to, że za każdym razem używany jest nowy bufor. Jeśli wykonujesz mnóstwo dodatkowych + operacji, nie jest to wydajne.
Dlaczego możesz uczynić to bardziej wydajnym:
Uwagi dotyczące wdrożenia:
Struktura danych liny:
Jeśli potrzebujesz naprawdę szybkich konkatenacji, rozważ użycie struktury danych liny .
źródło
Zarezerwuj ostatnie miejsce wcześniej, a następnie użyj metody dołączania z buforem. Załóżmy na przykład, że oczekujesz, że ostateczna długość ciągu będzie wynosić 1 milion znaków:
źródło
Nie martwiłbym się tym. Jeśli zrobisz to w pętli, łańcuchy zawsze będą wstępnie alokować pamięć, aby zminimalizować realokacje - po prostu użyj
operator+=
w takim przypadku. A jeśli robisz to ręcznie, coś takiego lub dłużejNastępnie tworzy tymczasowe - nawet jeśli kompilator mógłby wyeliminować niektóre kopie zwracanej wartości. Dzieje się tak, ponieważ w wywoływanym sukcesywnie
operator+
nie wie, czy parametr referencyjny odwołuje się do nazwanego obiektu, czy tymczasowo zwracany zoperator+
wywołania podrzędnego . Wolałbym się tym nie przejmować, zanim nie wykonam profilowania. Ale weźmy przykład, aby to pokazać. Najpierw wprowadzamy nawiasy, aby powiązanie było jasne. Umieszczam argumenty bezpośrednio po deklaracji funkcji, która jest używana dla przejrzystości. Poniżej pokazuję, jakie jest otrzymane wyrażenie:Teraz w tym dodatku
tmp1
jest to , co zostało zwrócone przez pierwsze wywołanie operatora + z przedstawionymi argumentami. Zakładamy, że kompilator jest naprawdę sprytny i optymalizuje kopię zwracanej wartości. W efekcie otrzymujemy jeden nowy ciąg zawierający konkatenacjęa
i" : "
. Teraz dzieje się tak:Porównaj to z następującymi:
Używa tej samej funkcji dla tymczasowego i nazwanego ciągu! Dlatego kompilator musi skopiować argument do nowego ciągu, dołączyć do niego i zwrócić go z treści
operator+
. Nie może wziąć wspomnienia czegoś tymczasowego i dołączyć do tego. Im większe wyrażenie, tym więcej kopii ciągów musi być zrobionych.Kolejne programy Visual Studio i GCC będą obsługiwać semantykę przenoszenia języka c ++ 1x (uzupełniającą semantykę kopiowania ) i odwołania do wartości rvalue jako dodatek eksperymentalny. To pozwala ustalić, czy parametr odnosi się do tymczasowego, czy nie. To sprawi, że takie dodatki będą zadziwiająco szybkie, ponieważ wszystko powyżej skończy się w jednym „potoku dodawania” bez kopii.
Jeśli okaże się, że jest to wąskie gardło, nadal możesz to zrobić
Do
append
rozmowy dołączy argument*this
, a następnie powrót odniesienie do siebie. Dlatego nie jest tam kopiowanie tymczasowych. Alternatywnieoperator+=
można użyć znaku, ale do ustalenia pierwszeństwa potrzebny byłby brzydki nawias.źródło
libstdc++
zaoperator+(string const& lhs, string&& rhs)
niereturn std::move(rhs.insert(0, lhs))
. Wtedy, jeśli oba są tymczasowe, tooperator+(string&& lhs, string&& rhs)
jeślilhs
ma wystarczającą dostępną pojemność, będzie to po prostu bezpośrednioappend()
. Tam, gdzie myślę, może to być wolniejsze niżoperator+=
jest, jeślilhs
nie ma wystarczającej pojemności, ponieważ wtedy wraca dorhs.insert(0, lhs)
, co musi nie tylko rozszerzyć bufor i dodać nową zawartość, jak np.append()
, Ale także musi przesuwać się wzdłuż oryginalnej zawartościrhs
prawej.operator+=
tego jest to, żeoperator+
nadal musi zwracać wartość, więc musi on zawieraćmove()
dowolny operand, do którego został dołączony. Mimo to wydaje mi się, że jest to dość niewielki narzut (kopiowanie kilku wskaźników / rozmiarów) w porównaniu z głębokim kopiowaniem całego ciągu, więc jest dobrze!W przypadku większości zastosowań to po prostu nie ma znaczenia. Po prostu napisz swój kod, błogo nieświadomy tego, jak dokładnie działa operator + i weź sprawy w swoje ręce tylko wtedy, gdy stanie się to pozornym wąskim gardłem.
źródło
W przeciwieństwie do .NET System.Strings, std :: strings w języku C ++ są modyfikowalne i dlatego można je budować za pomocą prostej konkatenacji tak samo szybko, jak za pomocą innych metod.
źródło
operator+
nie musi zwracać nowego ciągu. Implementatory mogą zwrócić jeden ze swoich operandów, zmodyfikowany, jeśli ten operand został przekazany przez odwołanie do wartości r.libstdc++
robi to na przykład . Tak więc, dzwoniącoperator+
z tymczasowymi, może osiągnąć taką samą lub prawie tak dobrą wydajność - co może być kolejnym argumentem za rezygnacją z niej, chyba że ma się testy porównawcze pokazujące, że stanowi wąskie gardło.może zamiast tego std :: stringstream?
Ale zgadzam się z opinią, że prawdopodobnie powinieneś po prostu zachować to w utrzymaniu i zrozumiałe, a następnie profil, aby zobaczyć, czy naprawdę masz problemy.
źródło
W Imperfect C ++ Matthew Wilson przedstawia dynamiczny konkatenator ciągów, który wstępnie oblicza długość końcowego ciągu, aby mieć tylko jedną alokację przed połączeniem wszystkich części. Możemy również zaimplementować statyczny konkatenator, grając z szablonami wyrażeń .
Ten rodzaj pomysłu został zaimplementowany w implementacji STLport std :: string - który nie jest zgodny ze standardem z powodu tego precyzyjnego hacka.
źródło
Glib::ustring::compose()
z powiązań glibmm do GLib robi to: szacuje ireserve()
s końcową długość w oparciu o dostarczony ciąg formatu i varargs, a następnieappend()
s każdy (lub jego sformatowany zamiennik) w pętli. Spodziewam się, że jest to dość powszechny sposób pracy.std::string
operator+
przydziela nowy łańcuch i za każdym razem kopiuje dwa łańcuchy operandów. powtarzać wiele razy i robi się drogie, O (n).std::string
append
aoperator+=
z drugiej strony, wpadać zdolności o 50% za każdym razem łańcuch musi rosnąć. Co znacznie zmniejsza liczbę alokacji pamięci i operacji kopiowania, O (log n).źródło
operator+
którym jeden lub oba argumenty są przekazywane przez odwołanie do wartości r, mogą uniknąć całkowitego przydzielenia nowego ciągu przez konkatenację do istniejącego bufora jeden z operandów (chociaż może być konieczne ponowne przydzielenie, jeśli ma niewystarczającą pojemność).W przypadku małych strun to nie ma znaczenia. Jeśli masz duże ciągi, lepiej przechowuj je w postaci w wektorze lub w innej kolekcji jako części. I dostosuj swój algorytm do pracy z takim zestawem danych zamiast z jednym dużym ciągiem.
Preferuję std :: ostringstream do złożonych konkatenacji.
źródło
Jak w przypadku większości rzeczy, łatwiej jest czegoś nie robić, niż to robić.
Jeśli chcesz wyprowadzić duże ciągi do GUI, może się zdarzyć, że cokolwiek wyprowadzasz, może obsługiwać ciągi w kawałkach lepiej niż jako duży ciąg (na przykład łączenie tekstu w edytorze tekstu - zwykle zachowują wiersze jako oddzielne Struktury).
Jeśli chcesz wyprowadzić dane do pliku, przesyłaj dane strumieniowo zamiast tworzenia dużego ciągu i wyprowadzania go.
Nigdy nie uważałem, że konieczne jest przyspieszenie konkatenacji, jeśli usunąłem niepotrzebną konkatenację z wolnego kodu.
źródło
Prawdopodobnie najlepsza wydajność, jeśli wstępnie przydzielisz (zarezerwujesz) miejsce w wynikowym ciągu.
Stosowanie:
źródło
Najszybsza jest prosta tablica znaków zamknięta w klasie, która śledzi rozmiar tablicy i liczbę przydzielonych bajtów.
Sztuczka polega na tym, aby na początku zrobić tylko jedną dużą alokację.
w
https://github.com/pedro-vicente/table-string
Benchmarki
W przypadku programu Visual Studio 2015, kompilacja debugowania x86, znaczna poprawa w stosunku do C ++ std :: string.
źródło
std::string
. Nie proszą o alternatywną klasę string.Możesz wypróbować ten z rezerwacjami pamięci dla każdego elementu:
źródło