Jak dokładnie std :: string_view jest szybszy niż const std :: string &?

221

std::string_viewdotarł do C ++ 17 i zaleca się używanie go zamiast const std::string&.

Jednym z powodów jest wydajność.

Czy ktoś może wyjaśnić, jak dokładnie std::string_view jest / będzie szybszy, niż const std::string&gdy zostanie użyty jako typ parametru? (załóżmy, że nie wykonano żadnych kopii w odbiorcy)

Patryk
źródło
7
std::string_viewjest tylko abstrakcją pary (char * begin, char * end). Używasz go, gdy robienie std::stringbyłby niepotrzebną kopią.
Pytanie C
Moim zdaniem nie chodzi o to, które z nich jest szybsze, ale o to, kiedy ich użyć. Jeśli potrzebuję manipulacji na łańcuchu i nie jest on trwały i / lub zachowuje oryginalną wartość, string_view jest idealny, ponieważ nie muszę wykonywać na nim kopii łańcucha. Ale jeśli muszę tylko sprawdzić coś na ciąg za pomocą na przykład string :: find, to odwołanie jest lepsze.
TheArquitect,
@QuestionC używasz go, gdy nie chcesz, aby twój API ograniczał się do std::string(string_view może akceptować surowe tablice, wektory, std::basic_string<>z domyślnymi alokatorami itp. Itd. Itd. Och, i oczywiście inne string_views)
patrz

Odpowiedzi:

213

std::string_view jest szybszy w kilku przypadkach.

Po pierwsze, std::string const&wymaga , aby dane znajdowały się w std::string, a nie w surowej tablicy C, char const*zwróconej przez C API, std::vector<char>wytworzonej przez jakiś silnik deserializacji itp. Unikana konwersja formatu pozwala uniknąć kopiowania bajtów i (jeśli łańcuch jest dłuższy niż SBO¹ dla konkretnej std::stringimplementacji) unika alokacji pamięci.

void foo( std::string_view bob ) {
  std::cout << bob << "\n";
}
int main(int argc, char const*const* argv) {
  foo( "This is a string long enough to avoid the std::string SBO" );
  if (argc > 1)
    foo( argv[1] );
}

W tej string_viewsprawie nie dokonuje się żadnych alokacji , ale byłoby, gdyby foowziął std::string const&zamiast zamiast string_view.

Drugim naprawdę ważnym powodem jest to, że pozwala na pracę z podciągami bez kopii. Załóżmy, że analizujesz 2-gigabajtowy ciąg Json (!) ². Jeśli go parsujesz std::string, każdy taki parsowany węzeł, w którym przechowują nazwę lub wartość węzła, kopiuje oryginalne dane z ciągu 2 gb do węzła lokalnego.

Zamiast tego, jeśli parsujesz to do std::string_views, węzły odnoszą się do oryginalnych danych. Pozwala to zaoszczędzić miliony przydziałów i zmniejszyć o połowę wymagania dotyczące pamięci podczas analizowania.

Przyspieszenie, które można uzyskać, jest po prostu śmieszne.

Jest to skrajny przypadek, ale inne przypadki „zdobycia podciągu i pracy z nim” również mogą generować przyzwoite przyspieszenia string_view.

Ważną częścią decyzji jest to, co tracisz, używając std::string_view. To niewiele, ale to jest coś.

Tracisz domniemane zerowe zakończenie i to wszystko. Więc jeśli ten sam ciąg zostanie przekazany do 3 funkcji, z których każda wymaga zerowego terminatora, konwersja na std::stringjeden raz może być mądra. Zatem jeśli wiadomo, że twój kod potrzebuje terminatora zerowego i nie oczekujesz ciągów zasilanych z buforów typu C lub podobnych, może weź std::string const&. W przeciwnym razie weź std::string_view.

Gdyby std::string_viewflaga oznaczała, że ​​jeśli jest zerowa (lub coś bardziej wymyślnego), usunęłaby nawet ten ostatni powód, aby użyć std::string const&.

Zdarzają się przypadki, w których wzięcie „ std::stringnie” const&jest optymalne w stosunku do std::string_view. Jeśli po wywołaniu musisz posiadać kopię łańcucha na czas nieokreślony, efektywne jest przyjmowanie wartości według. Będziesz albo w przypadku SBO (bez przydziałów, wystarczy kilka kopii znaków, aby go zduplikować), albo będziesz mógł przenieść bufor przydzielony na stos do lokalnego std::string. Posiadanie dwóch przeciążeń std::string&&i std::string_viewmoże być szybciej, ale tylko nieznacznie, a to spowodowałoby kodu skromną uwędzić (który może kosztować wszystkie zyski prędkość).


¹ Optymalizacja małego bufora

² Rzeczywisty przypadek użycia.

Jak - Adam Nevraumont
źródło
8
Tracisz także własność. Jest to interesujące tylko wtedy, gdy łańcuch zostanie zwrócony i może być czymś innym niż podłańcuch bufora, który z pewnością przetrwa wystarczająco długo. W rzeczywistości utrata własności to broń obosieczna.
Deduplicator
SBO brzmi dziwnie. Zawsze słyszałem SSO (optymalizacja małych ciągów)
phuclv
@ phu Sure; ale ciągi nie są jedyną rzeczą, na której używasz trików.
Yakk - Adam Nevraumont
@phuclv SSO to tylko konkretny przypadek SBO, który oznacza małą optymalizację bufora . Alternatywne warunki to wybór małych danych. , mały obiekt opt. lub mały rozmiar opt. .
Daniel Langr
59

Jednym ze sposobów poprawienia wydajności string_view jest to, że umożliwia łatwe usuwanie przedrostków i przyrostków. Pod maską string_view może po prostu dodać rozmiar prefiksu do wskaźnika do jakiegoś bufora łańcucha lub odjąć rozmiar sufiksu od licznika bajtów, zwykle jest to szybkie. z drugiej strony std :: string musi kopiować swoje bajty, gdy robisz coś takiego jak substr (w ten sposób otrzymujesz nowy ciąg, który jest właścicielem jego bufora, ale w wielu przypadkach po prostu chcesz uzyskać część oryginalnego ciągu bez kopiowania). Przykład:

std::string str{"foobar"};
auto bar = str.substr(3);
assert(bar == "bar");

Ze std :: string_view:

std::string str{"foobar"};
std::string_view bar{str.c_str(), str.size()};
bar.remove_prefix(3);
assert(bar == "bar");

Aktualizacja:

Napisałem bardzo prosty test porównawczy, aby dodać prawdziwe liczby. Użyłem niesamowitej biblioteki testów Google . Funkcje testowane to:

string remove_prefix(const string &str) {
  return str.substr(3);
}
string_view remove_prefix(string_view str) {
  str.remove_prefix(3);
  return str;
}
static void BM_remove_prefix_string(benchmark::State& state) {                
  std::string example{"asfaghdfgsghasfasg3423rfgasdg"};
  while (state.KeepRunning()) {
    auto res = remove_prefix(example);
    // auto res = remove_prefix(string_view(example)); for string_view
    if (res != "aghdfgsghasfasg3423rfgasdg") {
      throw std::runtime_error("bad op");
    }
  }
}
// BM_remove_prefix_string_view is similar, I skipped it to keep the post short

Wyniki

(x86_64 linux, gcc 6.2, „ -O3 -DNDEBUG”):

Benchmark                             Time           CPU Iterations
-------------------------------------------------------------------
BM_remove_prefix_string              90 ns         90 ns    7740626
BM_remove_prefix_string_view          6 ns          6 ns  120468514
Pavel Davydov
źródło
2
To wspaniale, że podałeś prawdziwy punkt odniesienia. To naprawdę pokazuje, co można uzyskać w odpowiednich przypadkach użycia.
Daniel Kamil Kozar,
1
@DanielKamilKozar Dzięki za opinie. Myślę też, że testy porównawcze są cenne, czasem zmieniają wszystko.
Pavel Davydov
47

Są 2 główne powody:

  • string_view jest plasterkiem w istniejącym buforze, nie wymaga przydziału pamięci
  • string_view jest przekazywany przez wartość, a nie przez odniesienie

Zalety posiadania wycinka są liczne:

  • możesz go używać z alokacją nowego bufora char const*lub char[]bez niego
  • możesz przenieść wiele plasterków i podsieci do istniejącego bufora bez przydzielania
  • podciąg to O (1), a nie O (N)
  • ...

Lepsza i bardziej spójna wydajność na całym świecie.


Przekazywanie przez wartość ma również zalety w porównaniu z przekazywaniem przez odniesienie, ponieważ aliasing.

W szczególności, gdy masz std::string const&parametr, nie ma gwarancji, że łańcuch odniesienia nie zostanie zmodyfikowany. W rezultacie kompilator musi ponownie pobrać treść ciągu po każdym wywołaniu do nieprzejrzystej metody (wskaźnik do danych, długość, ...).

Z drugiej strony, podczas przekazywania string_viewwartości przez, kompilator może statycznie ustalić, że żaden inny kod nie może modyfikować długości i wskaźników danych na stosie (lub w rejestrach). W rezultacie może „buforować” je między wywołaniami funkcji.

Matthieu M.
źródło
36

Jedną rzeczą, jaką może zrobić, jest uniknięcie budowy std::stringobiektu w przypadku niejawnej konwersji z łańcucha zakończonego znakiem zerowym:

void foo(const std::string& s);

...

foo("hello, world!"); // std::string object created, possible dynamic allocation.
char msg[] = "good morning!";
foo(msg); // std::string object created, possible dynamic allocation.
juanchopanza
źródło
12
Być może warto powiedzieć, że const std::string str{"goodbye!"}; foo(str);prawdopodobnie nie będzie szybciej z string_view niż z string &
Martin Bonner obsługuje Monikę
1
Czy nie string_viewbędzie powolny, ponieważ musi skopiować dwa wskaźniki zamiast jednego wskaźnika const string&?
balki
9

std::string_viewjest w zasadzie tylko opakowaniem const char*. A przekazywanie const char*oznacza, że ​​w systemie będzie o jeden wskaźnik mniej niż przekazywanie const string*(lub const string&), ponieważ string*implikuje coś takiego:

string* -> char* -> char[]
           |   string    |

Oczywiście w celu przekazywania argumentów const pierwszy wskaźnik jest zbędny.

ps Jeden substancial różnica między std::string_viewi const char*, mimo wszystko, jest to, że string_views nie muszą być zakończony zerem (mają wbudowany w rozmiarze), a to pozwala na losowe składanie w miejscu dłuższych łańcuchach.

n.caillou
źródło
4
O co chodzi z opiniami? std::string_viewsą po prostu fantazyjne const char*, kropka. GCC wdraża je w następujący sposób:class basic_string_view {const _CharT* _M_str; size_t _M_len;}
n.caillou
4
po prostu dostań się do 65 000 powtórzeń (z twojego obecnego 65), a to byłaby zaakceptowana odpowiedź (macha do tłumów kultowych) :)
mlvljr
7
@mlvljr Nikt nie przechodzi std::string const*. Ten schemat jest niezrozumiały. @ n.caillou: Twój własny komentarz jest już dokładniejszy niż odpowiedź. To string_viewcoś więcej niż „fantazyjne char const*” - to naprawdę dość oczywiste.
patrz
@sehe Mógłbym być taki, że nikt, nie ma problemu (tj. przekazanie wskaźnika (lub odwołania) do
stałego
2
@sehe Rozumiesz to z punktu widzenia optymalizacji lub wykonania std::string const*i std::string const&jesteś taki sam, prawda?
n.caillou