Effiziente Verkettung von Zeichenfolgen in C ++

108

Ich habe einige Leute gehört, die sich Sorgen über den Operator "+" in std :: string und verschiedene Problemumgehungen gemacht haben, um die Verkettung zu beschleunigen. Sind diese wirklich notwendig? Wenn ja, wie lassen sich Zeichenfolgen in C ++ am besten verketten?

sneg
quelle
13
Grundsätzlich ist das + KEIN Konzentrationsoperator (da es eine neue Zeichenfolge generiert). Verwenden Sie + = für die Verkettung.
Martin York
1
Seit C ++ 11 gibt es einen wichtigen Punkt: operator + kann einen seiner Operanden ändern und ihn per Move zurückgeben, wenn dieser Operand als rvalue-Referenz übergeben wurde. libstdc++ tut dies zum Beispiel . Wenn Sie operator + mit temporären Elementen aufrufen, kann dies zu einer nahezu ebenso guten Leistung führen - möglicherweise ein Argument für die Standardeinstellung aus Gründen der Lesbarkeit, es sei denn, es gibt Benchmarks, die belegen, dass es sich um einen Engpass handelt. Eine standardisierte Variable append()wäre jedoch sowohl optimal als auch lesbar ...
underscore_d

Antworten:

85

Die zusätzliche Arbeit lohnt sich wahrscheinlich nicht, es sei denn, Sie brauchen wirklich wirklich Effizienz. Sie werden wahrscheinlich eine viel bessere Effizienz erzielen, wenn Sie stattdessen den Operator + = verwenden.

Nach diesem Haftungsausschluss werde ich Ihre eigentliche Frage beantworten ...

Die Effizienz der STL-Zeichenfolgenklasse hängt von der Implementierung der von Ihnen verwendeten STL ab.

Sie könnten Effizienz garantieren und mehr Kontrolle haben indem Sie die Verkettung manuell über die integrierten c-Funktionen durchführen.

Warum Operator + nicht effizient ist:

Schauen Sie sich diese Oberfläche an:

template <class charT, class traits, class Alloc>
basic_string<charT, traits, Alloc>
operator+(const basic_string<charT, traits, Alloc>& s1,
          const basic_string<charT, traits, Alloc>& s2)

Sie können sehen, dass nach jedem + ein neues Objekt zurückgegeben wird. Das bedeutet, dass jedes Mal ein neuer Puffer verwendet wird. Wenn Sie eine Menge zusätzlicher + Operationen ausführen, ist dies nicht effizient.

Warum Sie es effizienter machen können:

  • Sie garantieren Effizienz, anstatt einem Delegierten zu vertrauen, dass er dies effizient für Sie erledigt
  • Die Klasse std :: string weiß nichts über die maximale Größe Ihres Strings und auch nicht darüber, wie oft Sie mit ihm verketten werden. Möglicherweise verfügen Sie über dieses Wissen und können Dinge tun, die auf diesen Informationen basieren. Dies führt zu weniger Neuzuweisungen.
  • Sie steuern die Puffer manuell, damit Sie sicher sein können, dass Sie nicht die gesamte Zeichenfolge in neue Puffer kopieren, wenn Sie dies nicht möchten.
  • Sie können den Stapel für Ihre Puffer anstelle des viel effizienteren Heaps verwenden.
  • Der Operator string + erstellt ein neues String-Objekt und gibt es unter Verwendung eines neuen Puffers zurück.

Überlegungen zur Umsetzung:

  • Verfolgen Sie die Saitenlänge.
  • Halten Sie einen Zeiger auf das Ende der Zeichenfolge und den Anfang oder nur auf den Anfang und verwenden Sie den Start + die Länge als Versatz, um das Ende der Zeichenfolge zu finden.
  • Stellen Sie sicher, dass der Puffer, in dem Sie Ihre Zeichenfolge speichern, groß genug ist, damit Sie keine Daten neu zuweisen müssen
  • Verwenden Sie strcpy anstelle von strcat, damit Sie nicht über die Länge der Zeichenfolge iterieren müssen, um das Ende der Zeichenfolge zu finden.

Seildatenstruktur:

Wenn Sie wirklich schnelle Verkettungen benötigen, sollten Sie eine Seildatenstruktur verwenden .

Brian R. Bondy
quelle
6
Hinweis: "STL" bezieht sich auf eine vollständig separate Open-Source-Bibliothek, die ursprünglich von HP stammt und teilweise als Grundlage für Teile der ISO-Standard-C ++ - Bibliothek verwendet wurde. "std :: string" war jedoch nie Teil der STL von HP, daher ist es völlig falsch, "STL" und "string" zusammen zu referenzieren.
James Curran
1
Ich würde nicht sagen, dass es falsch ist, STL und String zusammen zu verwenden. Siehe sgi.com/tech/stl/table_of_contents.html
Brian R. Bondy
1
Als SGI die Wartung der STL von HP übernahm, wurde sie nachgerüstet, um sie an die Standardbibliothek anzupassen (weshalb ich sagte, dass sie niemals Teil der STL von HP ist). Der Urheber von std :: string ist jedoch das ISO C ++ - Komitee.
James Curran
2
Randnotiz: Der SGI-Mitarbeiter, der viele Jahre für die Aufrechterhaltung der STL verantwortlich war, war Matt Austern, der gleichzeitig die Untergruppe Bibliothek des ISO C ++ - Standardisierungsausschusses leitete.
James Curran
4
Können Sie bitte klarstellen oder einige Punkte angeben, warum Sie den Stapel für Ihre Puffer anstelle des viel effizienteren Haufens verwenden können? ? Woher kommt dieser Effizienzunterschied?
7.
76

Reservieren Sie vorher Ihren endgültigen Speicherplatz und verwenden Sie dann die Append-Methode mit einem Puffer. Angenommen, Sie erwarten eine endgültige Zeichenfolgenlänge von 1 Million Zeichen:

std::string s;
s.reserve(1000000);

while (whatever)
{
  s.append(buf,len);
}
Carlos A. Ibarra
quelle
17

Ich würde mir darüber keine Sorgen machen. Wenn Sie dies in einer Schleife tun, weisen Zeichenfolgen immer Speicher zu, um Neuzuweisungen zu minimieren. Verwenden Sie dies nur operator+=in diesem Fall. Und wenn Sie es manuell tun, so etwas oder länger

a + " : " + c

Dann werden temporäre Dateien erstellt - auch wenn der Compiler einige Kopien von Rückgabewerten entfernen könnte. Dies liegt daran, dass in einem nacheinander aufgerufenen operator+Objekt nicht bekannt ist, ob der Referenzparameter auf ein benanntes Objekt oder ein von einem Unteraufruf zurückgegebenes temporäres Objekt verweist operator+. Ich würde mir lieber keine Sorgen machen, bevor ich nicht zuerst ein Profil erstellt habe. Aber nehmen wir ein Beispiel, um das zu zeigen. Wir führen zuerst Klammern ein, um die Bindung klar zu machen. Ich setze die Argumente direkt nach der Funktionsdeklaration, die der Klarheit halber verwendet wird. Darunter zeige ich, was der resultierende Ausdruck dann ist:

((a + " : ") + c) 
calls string operator+(string const&, char const*)(a, " : ")
  => (tmp1 + c)

In diesem Zusatz tmp1wurde nun der erste Aufruf von operator + mit den angezeigten Argumenten zurückgegeben. Wir gehen davon aus, dass der Compiler wirklich clever ist und die Rückgabewertkopie optimiert. Am Ende haben wir also eine neue Zeichenfolge, die die Verkettung von aund enthält " : ". Nun passiert dies:

(tmp1 + c)
calls string operator+(string const&, string const&)(tmp1, c)
  => tmp2 == <end result>

Vergleichen Sie das mit Folgendem:

std::string f = "hello";
(f + c)
calls string operator+(string const&, string const&)(f, c)
  => tmp1 == <end result>

Es verwendet dieselbe Funktion für eine temporäre und eine benannte Zeichenfolge! Der Compiler muss also das Argument in eine neue Zeichenfolge kopieren und an diese anhängen und aus dem Hauptteil von zurückgeben operator+. Es kann nicht die Erinnerung an eine temporäre nehmen und daran anhängen. Je größer der Ausdruck ist, desto mehr Kopien von Zeichenfolgen müssen erstellt werden.

Next Visual Studio und GCC unterstützen die Verschiebungssemantik von c ++ 1x (ergänzt die Kopiersemantik ) und die rvalue-Referenzen als experimentelle Ergänzung. Auf diese Weise können Sie herausfinden, ob der Parameter auf ein temporäres Element verweist oder nicht. Dies wird solche Ergänzungen erstaunlich schnell machen, da alle oben genannten in einer "Add-Pipeline" ohne Kopien enden werden.

Wenn sich herausstellt, dass es sich um einen Engpass handelt, können Sie dies dennoch tun

 std::string(a).append(" : ").append(c) ...

Die appendAufrufe hängen das Argument an *thisund geben dann einen Verweis auf sich selbst zurück. Dort wird also kein Provisorium kopiert. Alternativ operator+=kann das verwendet werden, aber Sie benötigen hässliche Klammern, um die Priorität festzulegen.

Johannes Schaub - litb
quelle
Ich musste überprüfen, ob stdlib-Implementierer dies wirklich tun. : P libstdc++für operator+(string const& lhs, string&& rhs)tut return std::move(rhs.insert(0, lhs)). Wenn dann beide temporär sind, wird es direkt direkt sein, operator+(string&& lhs, string&& rhs)wenn lhsgenügend Kapazität verfügbar ist append(). Wo ich denke, dass dies langsamer sein kann als operator+=wenn lhses nicht genügend Kapazität hat, wie es dann zurückfällt rhs.insert(0, lhs), was nicht nur den Puffer erweitern und die neuen Inhalte wie hinzufügen muss append(), sondern auch entlang des ursprünglichen Inhalts von rhsrechts verschoben werden muss .
underscore_d
Der andere Aufwand im Vergleich zu operator+=ist, dass operator+immer noch ein Wert zurückgegeben werden muss, also an den move()Operanden, an den er angehängt ist. Trotzdem denke ich, dass dies ein relativ geringer Aufwand ist (Kopieren einiger Zeiger / Größen) im Vergleich zum tiefen Kopieren der gesamten Zeichenfolge, also ist es gut!
underscore_d
11

Für die meisten Anwendungen spielt es einfach keine Rolle. Schreiben Sie einfach Ihren Code, ohne zu wissen, wie genau der Operator + funktioniert, und nehmen Sie die Angelegenheit nur dann selbst in die Hand, wenn dies zu einem offensichtlichen Engpass wird.

Pesto
quelle
7
Natürlich lohnt es sich in den meisten Fällen nicht, aber das beantwortet seine Frage nicht wirklich.
Brian R. Bondy
1
Ja. Ich bin damit einverstanden, nur zu sagen "Profil dann optimieren" kann als Kommentar auf die Frage gesetzt werden :)
Johannes Schaub - litb
6
Technisch fragte er, ob diese "notwendig" seien. Sie sind es nicht, und dies beantwortet diese Frage.
Samantha Branham
Fair genug, aber es wird definitiv für einige Anwendungen benötigt. In diesen Anwendungen reduziert sich die Antwort auf: "Nehmen Sie die Dinge selbst in die Hand"
Brian R. Bondy
4
@Pesto In der Programmierwelt gibt es eine perverse Vorstellung, dass Leistung keine Rolle spielt, und wir können den ganzen Deal einfach ignorieren, weil Computer immer schneller werden. Die Sache ist, das ist nicht der Grund, warum Leute in C ++ programmieren und das ist nicht der Grund, warum sie Fragen zum Stapelüberlauf über eine effiziente Verkettung von Zeichenfolgen stellen.
MrFox
7

Im Gegensatz zu .NET System.Strings, C ++ 's std :: strings sind wandelbar und können daher durch einfache Verkettung aufgebaut werden genauso schnell wie durch andere Methoden.

James Curran
quelle
2
Vor allem, wenn Sie Reserve () verwenden, um den Puffer vor dem Start groß genug für das Ergebnis zu machen.
Mark Ransom
Ich denke, er spricht über Operator + =. es verkettet auch, obwohl es ein entarteter Fall ist. james war ein vc ++ mvp, also erwarte ich, dass er eine Ahnung von c ++ hat: p
Johannes Schaub - litb
1
Ich bezweifle keine Sekunde, dass er über umfassende Kenntnisse in C ++ verfügt, nur dass es ein Missverständnis über die Frage gab. Die Frage nach der Effizienz von operator +, der bei jedem Aufruf neue Zeichenfolgenobjekte zurückgibt und daher neue Zeichenpuffer verwendet.
Brian R. Bondy
1
Ja. aber dann fragte er nach dem Falloperator + ist langsam, was der beste Weg ist, eine Verkettung durchzuführen. und hier kommt Operator + = ins Spiel. aber ich stimme zu, dass James 'Antwort ein wenig kurz ist. es klingt so, als könnten wir alle operator + verwenden und es ist am effizientesten: p
Johannes Schaub - litb
@ BrianR.Bondy operator+muss keinen neuen String zurückgeben. Implementierer können einen ihrer geänderten Operanden zurückgeben, wenn dieser Operand als rvalue-Referenz übergeben wurde. libstdc++ tut dies zum Beispiel . Wenn Sie also operator+mit temporären Geräten anrufen , kann dies die gleiche oder fast genauso gute Leistung erzielen - was ein weiteres Argument für die Standardeinstellung sein könnte, es sei denn, es gibt Benchmarks, die belegen, dass es sich um einen Engpass handelt.
underscore_d
4

In Imperfect C ++ präsentiert Matthew Wilson einen dynamischen Zeichenfolgenverketter, der die Länge der endgültigen Zeichenfolge vorberechnet, um nur eine Zuordnung zu erhalten, bevor alle Teile verkettet werden. Wir können auch einen statischen Verketter implementieren, indem wir mit Ausdrucksvorlagen spielen .

Diese Art von Idee wurde in der STLport std :: string-Implementierung implementiert - die aufgrund dieses präzisen Hacks nicht dem Standard entspricht.

Luc Hermitte
quelle
Glib::ustring::compose()Von den Glibmm-Bindungen zu GLib wird Folgendes ausgeführt: Schätzt und reserve()s die endgültige Länge basierend auf der bereitgestellten Formatzeichenfolge und den Varargs, dann append()s jede (oder ihre formatierte Ersetzung) in einer Schleife. Ich gehe davon aus, dass dies eine ziemlich übliche Arbeitsweise ist.
underscore_d
4

std::string operator+ordnet eine neue Zeichenfolge zu und kopiert jedes Mal die beiden Operandenzeichenfolgen. viele Male wiederholen und es wird teuer, O (n).

std::string appendund operator+=auf der anderen Seite stößt die Kapazität um 50% jedes Mal , wenn die Zeichenfolge wachsen muss. Dies reduziert die Anzahl der Speicherzuweisungen und Kopiervorgänge erheblich, O (log n).

Timmerov
quelle
Ich bin mir nicht ganz sicher, warum dies abgelehnt wurde. Die 50% -Zahl wird vom Standard nicht verlangt, aber IIRC, die oder 100% sind übliche Maßstäbe für das Wachstum in der Praxis. Alles andere in dieser Antwort scheint nicht zu beanstanden.
underscore_d
Monate später, nehme ich an, ist es nicht operator+allzu genau, da es lange nach dem Debüt von C ++ 11 geschrieben wurde und Überladungen, bei denen eines oder beide Argumente durch eine rvalue-Referenz übergeben werden, die Zuweisung einer neuen Zeichenfolge insgesamt vermeiden können, indem sie in den vorhandenen Puffer von verkettet werden einer der Operanden (obwohl sie möglicherweise neu zugewiesen werden müssen, wenn die Kapazität nicht ausreicht).
underscore_d
2

Für kleine Saiten spielt es keine Rolle. Wenn Sie große Zeichenfolgen haben, sollten Sie diese besser als Vektor oder in einer anderen Sammlung als Teile speichern. Und passen Sie Ihren Algorithmus an, um mit solchen Daten anstelle der einen großen Zeichenfolge zu arbeiten.

Ich bevorzuge std :: ostringstream für komplexe Verkettung.

Mykola Golubyev
quelle
2

Wie bei den meisten Dingen ist es einfacher, etwas nicht zu tun, als es zu tun.

Wenn Sie große Zeichenfolgen an eine GUI ausgeben möchten, kann es sein, dass alles, was Sie ausgeben, die Zeichenfolgen in Teilen besser verarbeiten kann als eine große Zeichenfolge (z. B. das Verketten von Text in einem Texteditor - normalerweise werden die Zeilen getrennt Strukturen).

Wenn Sie in eine Datei ausgeben möchten, streamen Sie die Daten, anstatt eine große Zeichenfolge zu erstellen und diese auszugeben.

Ich habe nie die Notwendigkeit gefunden, die Verkettung schneller zu machen, wenn ich unnötige Verkettung aus langsamem Code entfernt habe.

Pete Kirkham
quelle
2

Wahrscheinlich die beste Leistung, wenn Sie Speicherplatz in der resultierenden Zeichenfolge vorab zuweisen (reservieren).

template<typename... Args>
std::string concat(Args const&... args)
{
    size_t len = 0;
    for (auto s : {args...})  len += strlen(s);

    std::string result;
    result.reserve(len);    // <--- preallocate result
    for (auto s : {args...})  result += s;
    return result;
}

Verwendung:

std::string merged = concat("This ", "is ", "a ", "test!");
LanDenLabs
quelle
0

Ein einfaches Array von Zeichen, das in einer Klasse gekapselt ist, die die Arraygröße und die Anzahl der zugewiesenen Bytes verfolgt, ist am schnellsten.

Der Trick besteht darin, zu Beginn nur eine große Zuordnung vorzunehmen.

beim

https://github.com/pedro-vicente/table-string

Benchmarks

Für Visual Studio 2015, x86-Debug-Build, wesentliche Verbesserung gegenüber C ++ std :: string.

| API                   | Seconds           
| ----------------------|----| 
| SDS                   | 19 |  
| std::string           | 11 |  
| std::string (reserve) | 9  |  
| table_str_t           | 1  |  
Pedro Vicente
quelle
1
Das OP interessiert sich für eine effiziente Verkettung std::string. Sie fragen nicht nach einer alternativen Zeichenfolgenklasse.
underscore_d
0

Sie können dies mit Speicherreservierungen für jedes Element versuchen:

namespace {
template<class C>
constexpr auto size(const C& c) -> decltype(c.size()) {
  return static_cast<std::size_t>(c.size());
}

constexpr std::size_t size(const char* string) {
  std::size_t size = 0;
  while (*(string + size) != '\0') {
    ++size;
  }
  return size;
}

template<class T, std::size_t N>
constexpr std::size_t size(const T (&)[N]) noexcept {
  return N;
}
}

template<typename... Args>
std::string concatStrings(Args&&... args) {
  auto s = (size(args) + ...);
  std::string result;
  result.reserve(s);
  return (result.append(std::forward<Args>(args)), ...);
}
Voltento
quelle