W pytaniu C ++ dotyczącym optymalizacji i stylu kodu kilka odpowiedzi odnosiło się do „logowania jednokrotnego” w kontekście optymalizacji kopii std::string
. Co w tym kontekście oznacza SSO?
Oczywiście nie jest to „pojedyncze logowanie”. Może „optymalizacja współdzielonych ciągów”?
c++
string
optimization
Raedwald
źródło
źródło
std::string
zaimplementowana”, a inna „co oznacza SSO”, musisz być absolutnie szalony, aby uznać je za to samo pytanieOdpowiedzi:
Tło / przegląd
Operacje na zmiennych automatycznych („ze stosu”, które są zmiennymi tworzonymi bez wywoływania
malloc
/new
) są generalnie znacznie szybsze niż operacje dotyczące wolnego magazynu („sterty”, czyli zmiennych tworzonych za pomocąnew
). Jednak rozmiar tablic automatycznych jest ustalany w czasie kompilacji, ale rozmiar tablic z bezpłatnego magazynu nie. Co więcej, rozmiar stosu jest ograniczony (zwykle kilka MiB), podczas gdy wolny magazyn jest ograniczony tylko pamięcią systemu.SSO to optymalizacja krótkich / małych ciągów. A
std::string
zazwyczaj przechowuje ciąg jako wskaźnik do wolnego magazynu („sterty”), co daje podobną charakterystykę wydajności, jak w przypadku wywołanianew char [size]
. Zapobiega to przepełnieniu stosu w przypadku bardzo dużych ciągów, ale może być wolniejsze, zwłaszcza w przypadku operacji kopiowania. W ramach optymalizacji wiele implementacjistd::string
tworzy małą automatyczną tablicę, coś w rodzajuchar [20]
. Jeśli masz ciąg o długości 20 znaków lub mniejszy (w tym przykładzie rzeczywisty rozmiar jest różny), zapisuje go bezpośrednio w tej tablicy. Dzięki temunew
w ogóle nie trzeba dzwonić , co nieco przyspiesza.EDYTOWAĆ:
Nie spodziewałem się, że ta odpowiedź będzie tak popularna, ale skoro tak, przedstawię bardziej realistyczną implementację, z zastrzeżeniem, że nigdy nie czytałem żadnej implementacji SSO „na wolności”.
Szczegóły dotyczące wdrożenia
Należy co najmniej
std::string
przechowywać następujące informacje:Rozmiar może być przechowywany jako a
std::string::size_type
lub jako wskaźnik do końca. Jedyną różnicą jest to, czy chcesz odjąć dwa wskaźniki, gdy użytkownik wywołuje,size
czy dodaćsize_type
do wskaźnika, gdy użytkownik wywołujeend
. Pojemność można przechowywać w dowolny sposób.Nie płacisz za to, czego nie używasz.
Najpierw rozważ naiwną implementację opartą na tym, co opisałem powyżej:
W przypadku systemu 64-bitowego oznacza to ogólnie, że
std::string
ma 24 bajty „narzutu” na łańcuch plus kolejne 16 na bufor SSO (16 wybranych tutaj zamiast 20 ze względu na wymagania dotyczące wypełnienia). Naprawdę nie miałoby sensu przechowywanie tych trzech elementów danych oraz lokalnej tablicy znaków, jak w moim uproszczonym przykładzie. Jeślim_size <= 16
, to umieszczę wszystkie danem_sso
, więc znam już pojemność i nie potrzebuję wskaźnika do danych. Jeślim_size > 16
, to nie potrzebujęm_sso
. Tam, gdzie potrzebuję ich wszystkich, nie ma absolutnie żadnego pokrycia. Inteligentniejsze rozwiązanie, które nie marnuje miejsca, wyglądałoby mniej więcej tak (nieprzetestowane, tylko do celów przykładowych):Zakładam, że większość implementacji wygląda bardziej tak.
źródło
std::string const &
, uzyskanie danych odbywa się w pojedynczym kierunku pamięci, ponieważ dane są przechowywane w miejscu odniesienia. Gdyby nie było optymalizacji małych ciągów, dostęp do danych wymagałby dwóch pośrednich operacji pamięciowych (najpierw do załadowania odwołania do ciągu i odczytania jego zawartości, a następnie do odczytania zawartości wskaźnika danych w ciągu).SSO to skrót od „Small String Optimization”, techniki, w której małe ciągi znaków są osadzane w treści klasy ciągów zamiast używać oddzielnie przydzielanego buforu.
źródło
Jak już wyjaśniono w innych odpowiedziach, SSO oznacza optymalizację małych / krótkich ciągów . Motywacją stojącą za tą optymalizacją jest niezaprzeczalny dowód na to, że aplikacje generalnie obsługują znacznie więcej krótszych ciągów niż dłuższe.
Jak wyjaśnił David Stone w swojej odpowiedzi powyżej ,
std::string
klasa używa wewnętrznego bufora do przechowywania zawartości o określonej długości, co eliminuje potrzebę dynamicznego przydzielania pamięci. Dzięki temu kod jest wydajniejszy i szybszy .Ta inna powiązana odpowiedź wyraźnie pokazuje, że rozmiar wewnętrznego bufora zależy od
std::string
implementacji, która różni się w zależności od platformy (patrz wyniki testów porównawczych poniżej).Benchmarki
Oto mały program, który porównuje operacje kopiowania wielu ciągów o tej samej długości. Zaczyna drukować czas kopiowania 10 milionów ciągów o długości = 1. Następnie powtarza się z ciągami o długości = 2. Powtarza się, aż długość wyniesie 50.
Jeśli chcesz uruchomić ten program, powinieneś to zrobić
./a.out > /dev/null
tak, aby czas na wydrukowanie łańcuchów nie był liczony. Liczby, które mają znaczenie, są drukowanestderr
, więc pojawią się w konsoli.Stworzyłem wykresy z danymi wyjściowymi z moich komputerów MacBook i Ubuntu. Zauważ, że następuje duży skok w czasie kopiowania ciągów, gdy długość osiągnie określony punkt. To jest moment, w którym ciągi znaków nie mieszczą się już w wewnętrznym buforze i trzeba użyć alokacji pamięci.
Zauważ również, że na komputerze z systemem Linux skok ma miejsce, gdy długość łańcucha osiągnie 16. Na Macbooku skok ma miejsce, gdy długość osiągnie 23. Potwierdza to, że SSO zależy od implementacji platformy.
Ubuntu
Macbook Pro
źródło