Wie genau ist std :: string_view schneller als const std :: string &?

221

std::string_viewhat es auf C ++ 17 geschafft und es wird allgemein empfohlen, es anstelle von zu verwenden const std::string&.

Einer der Gründe ist die Leistung.

Kann jemand erklären, wie genau std::string_view schneller ist / sein wird als const std::string&bei Verwendung als Parametertyp? (Nehmen wir an, es werden keine Kopien im Angerufenen angefertigt.)

Patryk
quelle
7
std::string_viewist nur eine Abstraktion des Paares (char * begin, char * end). Sie verwenden es, wenn std::stringeine unnötige Kopie erstellt wird.
QuestionC
Meiner Meinung nach ist die Frage nicht genau, welche schneller ist, sondern wann man sie benutzt. Wenn ich eine Manipulation an der Zeichenfolge benötige und diese nicht permanent ist und / oder den ursprünglichen Wert beibehält, ist string_view perfekt, da ich keine Kopie der Zeichenfolge erstellen muss. Aber wenn ich zum Beispiel nur mit string :: find etwas an einem String überprüfen muss, ist die Referenz besser.
TheArquitect
@QuestionC Sie verwenden es, wenn Sie nicht möchten, dass sich Ihre API auf beschränkt std::string(string_view kann Raw-Arrays, Vektoren, std::basic_string<>mit nicht standardmäßigen Allokatoren usw. usw. usw. akzeptieren . Oh, und andere string_views natürlich)
sehe

Antworten:

213

std::string_view ist in einigen Fällen schneller.

Erstens std::string const&müssen sich die Daten in einem std::stringund nicht in einem rohen C-Array befinden, das char const*von einer C-API zurückgegeben wird, std::vector<char>von einer Deserialisierungs-Engine erzeugt wird usw. Die vermiedene Formatkonvertierung vermeidet das Kopieren von Bytes und (wenn die Zeichenfolge länger als die ist SBO¹ für die jeweilige std::stringImplementierung) vermeidet eine Speicherzuordnung.

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] );
}

In dem string_viewFall werden keine Zuweisungen vorgenommen , aber es würde geben, wenn fooa std::string const&anstelle von a genommen würde string_view.

Der zweite wirklich große Grund ist, dass es das Arbeiten mit Teilzeichenfolgen ohne Kopie ermöglicht. Angenommen, Sie analysieren eine 2-Gigabyte-JSON-Zeichenfolge (!) ². Wenn Sie es analysieren std::string, kopiert jeder dieser Analyseknoten, auf dem der Name oder Wert eines Knotens gespeichert ist, die Originaldaten aus der 2-GB-Zeichenfolge auf einen lokalen Knoten.

Wenn Sie es stattdessen auf std::string_views analysieren , beziehen sich die Knoten auf die Originaldaten. Dies kann Millionen von Zuordnungen einsparen und den Speicherbedarf während des Parsens halbieren.

Die Beschleunigung, die Sie bekommen können, ist einfach lächerlich.

Dies ist ein extremer Fall, aber auch andere Fälle, in denen Sie einen Teilstring erhalten und damit arbeiten, können zu angemessenen Beschleunigungen führen string_view.

Ein wichtiger Teil der Entscheidung ist, was Sie durch die Verwendung verlieren std::string_view. Es ist nicht viel, aber es ist etwas.

Sie verlieren die implizite Nullterminierung, und das war's auch schon. Wenn also dieselbe Zeichenfolge an drei Funktionen übergeben wird, für die alle ein Nullterminator erforderlich ist, kann eine Konvertierung in eine std::stringeinmal sinnvoll sein. Wenn bekannt ist, dass Ihr Code einen Null-Terminator benötigt und Sie keine Zeichenfolgen erwarten, die aus Puffern im C-Stil oder ähnlichem gespeist werden, nehmen Sie möglicherweise a std::string const&. Ansonsten nimm ein std::string_view.

Wenn std::string_viewein Flag angegeben wäre, ob es nullterminiert ist (oder etwas ausgefalleneres), würde es sogar den letzten Grund für die Verwendung von a entfernen std::string const&.

Es gibt einen Fall, in dem die Einnahme von a std::stringmit no const&über a optimal ist std::string_view. Wenn Sie nach dem Aufruf auf unbestimmte Zeit eine Kopie der Zeichenfolge besitzen müssen, ist die Verwendung von By-Value effizient. Sie werden entweder in der SBO Fall sein (und keine Zuweisungen, nur wenige Zeichen kopiert sie zu kopieren), oder Sie werden in der Lage sein zu bewegen den Heap-zugewiesenen Puffer in eine lokale std::string. Mit zwei Überlastungen std::string&&undstd::string_view könnte schneller sein, aber nur geringfügig, und es wäre bescheiden Code aufblähen verursachen (die Sie alle der Geschwindigkeit Gewinne kosten könnte).


¹ Optimierung kleiner Puffer

² Tatsächlicher Anwendungsfall.

Yakk - Adam Nevraumont
quelle
8
Sie verlieren auch den Besitz. Dies ist nur dann von Interesse, wenn die Zeichenfolge zurückgegeben wird und es sich möglicherweise um eine andere Zeichenfolge als eine Teilzeichenfolge eines Puffers handeln muss, die garantiert lange genug überlebt. Tatsächlich ist der Verlust des Eigentums eine sehr zweischneidige Waffe.
Deduplikator
SBO klingt seltsam. Ich habe immer SSO (Small String Optimization) gehört
phuclv
@phu Sicher; Aber Strings sind nicht das einzige, bei dem Sie den Trick anwenden.
Yakk - Adam Nevraumont
@phuclv SSO ist nur ein spezieller Fall von SBO, der für Small Buffer Optimization steht . Alternative Begriffe sind Small Data Opt. , kleines Objekt opt. oder klein opt. .
Daniel Langr
59

Eine Möglichkeit, mit der string_view die Leistung verbessert, besteht darin, dass Präfixe und Suffixe einfach entfernt werden können. Unter der Haube kann string_view einfach die Präfixgröße zu einem Zeiger auf einen Zeichenfolgenpuffer hinzufügen oder die Suffixgröße vom Bytezähler subtrahieren. Dies ist normalerweise schnell. std :: string hingegen muss seine Bytes kopieren, wenn Sie so etwas wie substr ausführen (auf diese Weise erhalten Sie einen neuen String, dem der Puffer gehört, aber in vielen Fällen möchten Sie nur einen Teil des ursprünglichen Strings abrufen, ohne ihn zu kopieren). Beispiel:

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

Mit std :: string_view:

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

Aktualisieren:

Ich habe einen sehr einfachen Benchmark geschrieben, um einige reelle Zahlen hinzuzufügen. Ich habe eine großartige Google Benchmark-Bibliothek verwendet . Benchmarked-Funktionen sind:

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

Ergebnisse

(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
quelle
2
Es ist großartig, dass Sie einen tatsächlichen Benchmark bereitgestellt haben. Dies zeigt wirklich, was in relevanten Anwendungsfällen erreicht werden kann.
Daniel Kamil Kozar
1
@ DanielKamilKozar Danke für das Feedback. Ich denke auch, dass Benchmarks wertvoll sind, manchmal ändern sie alles.
Pavel Davydov
47

Es gibt zwei Hauptgründe:

  • string_view ist ein Slice in einem vorhandenen Puffer, erfordert keine Speicherzuordnung
  • string_view wird als Wert übergeben, nicht als Referenz

Die Vorteile einer Scheibe sind vielfältig:

  • Sie können es mit char const*oder char[]ohne Zuweisung eines neuen Puffers verwenden
  • Sie können mehrere Slices und Subslices in einen vorhandenen Puffer aufnehmen, ohne sie zuzuweisen
  • Teilzeichenfolge ist O (1), nicht O (N)
  • ...

Überall bessere und gleichmäßigere Leistung.


Das Übergeben von Werten hat auch Vorteile gegenüber dem Übergeben von Referenzen, da Aliasing.

Insbesondere, wenn Sie eine haben std::string const& Parameter haben, gibt es keine Garantie dafür, dass die Referenzzeichenfolge nicht geändert wird. Infolgedessen muss der Compiler den Inhalt der Zeichenfolge nach jedem Aufruf in eine undurchsichtige Methode (Zeiger auf Daten, Länge, ...) erneut abrufen.

Andererseits string_viewkann der Compiler beim Übergeben eines By-Werts statisch bestimmen, dass kein anderer Code die Länge und die Datenzeiger ändern kann, die sich jetzt auf dem Stapel (oder in Registern) befinden. Infolgedessen kann es sie über Funktionsaufrufe hinweg "zwischenspeichern".

Matthieu M.
quelle
36

Eine Möglichkeit besteht darin, die Erstellung eines std::stringObjekts bei einer impliziten Konvertierung aus einer nullterminierten Zeichenfolge zu vermeiden :

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
quelle
12
Es könnte erwähnenswert sein, dass const std::string str{"goodbye!"}; foo(str);es mit string_view wahrscheinlich nicht schneller geht als mit string &
Martin Bonner unterstützt Monica
1
Wird es string_viewnicht langsam sein, da zwei Zeiger im Gegensatz zu einem Zeiger kopiert werden müssen const string&?
Balki
9

std::string_view ist im Grunde nur ein Wrapper um ein const char* . Und Übergeben const char*bedeutet, dass es im Vergleich zum Übergeben const string*(oder const string&) einen Zeiger weniger im System gibt , weil dies string*Folgendes impliziert:

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

Für die Übergabe von const-Argumenten ist der erste Zeiger eindeutig überflüssig.

ps Ein wesentlicher Unterschied zwischen std::string_viewundconst char* wesentlicher besteht dennoch darin, dass die string_views nicht nullterminiert werden müssen (sie haben eine integrierte Größe), und dies ermöglicht das zufällige direkte Spleißen längerer Strings.

n.caillou
quelle
4
Was ist mit den Abstimmungen? std::string_views sind nur ausgefallene const char*s, Punkt. GCC implementiert sie wie class basic_string_view {const _CharT* _M_str; size_t _M_len;}
folgt
4
Erreichen Sie
7
@mlvljr Niemand geht vorbei std::string const*. Und dieses Diagramm ist unverständlich. @ n.caillou: Dein eigener Kommentar ist schon genauer als die Antwort. Das macht string_viewmehr als "schick char const*" - es ist wirklich ganz offensichtlich.
sehe
@sehe Ich könnte sein, dass niemand, kein Problem (dh einen Zeiger (oder eine Referenz) auf eine konstante Zeichenfolge übergeben, warum nicht?) :)
mlvljr
2
@sehe Du verstehst das aus Sicht der Optimierung oder Ausführung std::string const*und std::string const&bist es auch, oder?
n.caillou