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łania new 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 implementacji std::string
tworzy małą automatyczną tablicę, coś w rodzaju char [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 temu new
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
- Pojemność
- Lokalizacja danych
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łuje end
. 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:
class string {
public:
// all 83 member functions
private:
std::unique_ptr<char[]> m_data;
size_type m_size;
size_type m_capacity;
std::array<char, 16> m_sso;
};
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śli m_size <= 16
, to umieszczę wszystkie dane m_sso
, więc znam już pojemność i nie potrzebuję wskaźnika do danych. Jeśli m_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):
class string {
public:
// all 83 member functions
private:
size_type m_size;
union {
class {
// This is probably better designed as an array-like class
std::unique_ptr<char[]> m_data;
size_type m_capacity;
} m_large;
std::array<char, sizeof(m_large)> m_small;
};
};
Zakładam, że większość implementacji wygląda bardziej tak.
std::string
zaimplementowana”, a inna „co oznacza SSO”, musisz być absolutnie szalony, aby uznać je za to samo pytanie