Concatenazione di stringhe efficiente in C ++


108

Ho sentito alcune persone esprimere preoccupazioni riguardo all'operatore "+" in std :: string e varie soluzioni alternative per accelerare la concatenazione. Qualcuno di questi è davvero necessario? In tal caso, qual è il modo migliore per concatenare le stringhe in C ++?


13
Fondamentalmente il + NON è un operatore di concatenazione (poiché genera una nuova stringa). Usa + = per la concatenazione.
Martin York

1
Dal momento che C ++ 11, c'è un punto importante: l'operatore + può modificare uno dei suoi operandi e restituirlo spostandolo se quell'operando è stato passato per riferimento rvalue. libstdc++ fa questo, per esempio . Quindi, quando si chiama l'operatore + con i provvisori, è possibile ottenere prestazioni quasi altrettanto buone, forse un argomento a favore del suo default, per motivi di leggibilità, a meno che non si disponga di benchmark che dimostrino che è un collo di bottiglia. Tuttavia, un variadico standardizzato append()sarebbe ottimale e leggibile ...
underscore_d

Risposte:


85

Il lavoro extra probabilmente non ne vale la pena, a meno che tu non abbia davvero bisogno di efficienza. Probabilmente avrai un'efficienza molto migliore semplicemente usando l'operatore + = invece.

Ora, dopo questo disclaimer, risponderò alla tua vera domanda ...

L'efficienza della classe di stringhe STL dipende dall'implementazione di STL che stai utilizzando.

Potresti garantire l'efficienza e avere un maggiore controllo da solo eseguendo la concatenazione manualmente tramite le funzioni integrate c.

Perché operator + non è efficiente:

Dai un'occhiata a questa interfaccia:

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)

Puoi vedere che un nuovo oggetto viene restituito dopo ogni +. Ciò significa che ogni volta viene utilizzato un nuovo buffer. Se stai facendo un sacco di + operazioni extra, non è efficiente.

Perché puoi renderlo più efficiente:

  • Stai garantendo efficienza invece di fidarti di un delegato che lo faccia in modo efficiente per te
  • la classe std :: string non sa nulla della dimensione massima della tua stringa, né quanto spesso ti concatenerai. Potresti avere questa conoscenza e puoi fare cose sulla base di avere queste informazioni. Ciò porterà a meno riassegnazioni.
  • Controllerai manualmente i buffer in modo da essere sicuro di non copiare l'intera stringa in nuovi buffer quando non vuoi che ciò accada.
  • Puoi usare lo stack per i tuoi buffer invece dell'heap che è molto più efficiente.
  • stringa + operatore creerà un nuovo oggetto stringa e lo restituirà quindi utilizzando un nuovo buffer.

Considerazioni per l'implementazione:

  • Tieni traccia della lunghezza della stringa.
  • Mantieni un puntatore alla fine della stringa e all'inizio, o solo all'inizio e usa l'inizio + la lunghezza come offset per trovare la fine della stringa.
  • Assicurati che il buffer in cui stai memorizzando la stringa sia abbastanza grande da non dover riallocare i dati
  • Usa strcpy invece di strcat in modo da non dover iterare sulla lunghezza della stringa per trovare la fine della stringa.

Struttura dati fune:

Se hai bisogno di concatenazioni molto veloci, considera l'utilizzo di una struttura dati della fune .


6
Nota: "STL" si riferisce a una libreria open source completamente separata, originariamente da HP, alcune parti della quale sono state utilizzate come base per parti della libreria C ++ standard ISO. "std :: string", tuttavia, non ha mai fatto parte dell'STL di HP, quindi è completamente sbagliato fare riferimento a "STL e" string "insieme.
James Curran

1
Non direi che sia sbagliato usare STL e string insieme. Vedi sgi.com/tech/stl/table_of_contents.html
Brian R. Bondy

1
Quando SGI ha rilevato la manutenzione dell'STL da HP, è stato adattato per adattarsi alla libreria standard (motivo per cui ho detto "non fa mai parte dell'STL di HP"). Tuttavia, il creatore di std :: string è il Comitato ISO C ++.
James Curran

2
Nota a margine: Il dipendente SGI che è stato incaricato di mantenere l'STL per molti anni era Matt Austern, che, allo stesso tempo, era a capo del sottogruppo Library dell'ISO C ++ Standardization Committee.
James Curran

4
Puoi per favore chiarire o dare alcuni punti sul perché puoi usare lo stack per i tuoi buffer invece dell'heap che è molto più efficiente. ? Da dove viene questa differenza di efficienza?
h7r

76

Prenota prima il tuo spazio finale, quindi usa il metodo append con un buffer. Ad esempio, supponi di aspettarti che la lunghezza della stringa finale sia di 1 milione di caratteri:

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

while (whatever)
{
  s.append(buf,len);
}

17

Non me ne preoccuperei. Se lo fai in un ciclo, le stringhe preallocheranno sempre la memoria per ridurre al minimo le riallocazioni, basta usarle operator+=in quel caso. E se lo fai manualmente, qualcosa di simile o più lungo

a + " : " + c

Quindi crea dei provvisori, anche se il compilatore potrebbe eliminare alcune copie del valore di ritorno. Questo perché in un richiamo successivo operator+non sa se il parametro di riferimento fa riferimento a un oggetto denominato oa un temporaneo restituito da una sottovocazione operator+. Preferirei non preoccuparmene prima di non aver prima profilato. Ma facciamo un esempio per dimostrarlo. Per prima cosa introduciamo le parentesi per rendere chiara la rilegatura. Metto gli argomenti direttamente dopo la dichiarazione di funzione utilizzata per chiarezza. Di seguito, mostro qual è l'espressione risultante:

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

Ora, in aggiunta, tmp1è ciò che è stato restituito dalla prima chiamata all'operatore + con gli argomenti mostrati. Supponiamo che il compilatore sia davvero intelligente e ottimizzi la copia del valore restituito. Quindi finiamo con una nuova stringa che contiene la concatenazione di ae " : ". Ora, questo accade:

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

Confrontalo con quanto segue:

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

Usa la stessa funzione per una stringa temporanea e per una stringa con nome! Quindi il compilatore deve copiare l'argomento in una nuova stringa e aggiungervi e restituirlo dal corpo di operator+. Non si può prendere il ricordo di un temporaneo e aggiungerlo a quello. Più grande è l'espressione, più copie di stringhe devono essere eseguite.

Successivamente Visual Studio e GCC supporteranno la semantica di spostamento di c ++ 1x (complementare alla semantica di copia ) e i riferimenti rvalue come aggiunta sperimentale. Ciò consente di capire se il parametro fa riferimento a un temporaneo o meno. Ciò renderà tali aggiunte incredibilmente veloci, poiché tutto quanto sopra finirà in una "pipeline di aggiunta" senza copie.

Se risulta essere un collo di bottiglia, puoi comunque farlo

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

Le appendchiamate aggiungono l'argomento a *thise quindi restituiscono un riferimento a se stesse. Quindi nessuna copia dei provvisori viene eseguita lì. O in alternativa, operator+=può essere utilizzato, ma per correggere la precedenza sarebbero necessarie brutte parentesi.


Ho dovuto controllare che gli implementatori di stdlib lo facessero davvero. : P libstdc++per operator+(string const& lhs, string&& rhs)fa return std::move(rhs.insert(0, lhs)). Quindi se entrambi sono provvisori, operator+(string&& lhs, string&& rhs)se lhsha una capacità sufficiente disponibile lo farà direttamente append(). Dove penso che questo rischia di essere più lento di quello che operator+=è se lhsnon ha abbastanza capacità, poiché poi ricade a rhs.insert(0, lhs), che non solo deve estendere il buffer e aggiungere nuovi contenuti come append(), ma deve anche spostarsi lungo i contenuti originali di rhsdestra.
underscore_d

L'altro pezzo di overhead rispetto a operator+=è che operator+deve comunque restituire un valore, quindi deve essere a move()qualsiasi operando a cui è stato aggiunto. Tuttavia, immagino che sia un sovraccarico abbastanza minore (copiare un paio di puntatori / dimensioni) rispetto alla copia profonda dell'intera stringa, quindi è buono!
underscore_d

11

Per la maggior parte delle applicazioni, non importa. Scrivi il tuo codice, beatamente inconsapevole di come funziona esattamente + l'operatore, e prendi in mano la situazione solo se diventa un apparente collo di bottiglia.


7
Ovviamente non ne vale la pena per la maggior parte dei casi, ma questo non risponde alla sua domanda.
Brian R. Bondy

1
si. Sono d'accordo che solo dicendo "profilo quindi ottimizza" può essere
aggiunto

6
Tecnicamente, ha chiesto se questi sono "necessari". Non lo sono, e questo risponde a questa domanda.
Samantha Branham

Abbastanza giusto, ma è decisamente necessario per alcune applicazioni. Quindi in quelle applicazioni la risposta si riduce a: "prendi in mano la situazione"
Brian R. Bondy

4
@ Pesto C'è un'idea perversa nel mondo della programmazione che le prestazioni non contano e che possiamo ignorare l'intero affare perché i computer continuano a diventare più veloci. Il fatto è che non è per questo che le persone programmano in C ++ e non è per questo che pubblicano domande sullo stack overflow sulla concatenazione efficiente delle stringhe.
MrFox

7

A differenza di .NET System.Strings, le stringhe std :: di C ++ sono modificabili e pertanto possono essere create tramite una semplice concatenazione con la stessa rapidità con cui si utilizzano altri metodi.


2
Soprattutto se usi reserve () per rendere il buffer abbastanza grande per il risultato prima di iniziare.
Mark Ransom

penso che stia parlando dell'operatore + =. è anche concatenante, sebbene sia un caso degenerato. james era un vc ++ mvp quindi mi aspetto che abbia qualche indizio di c ++: p
Johannes Schaub - litb

1
Non dubito per un secondo che abbia una vasta conoscenza di C ++, solo che c'è stato un malinteso sulla domanda. La domanda posta sull'efficienza di operator + che restituisce nuovi oggetti stringa ogni volta che viene chiamato e quindi utilizza nuovi buffer di caratteri.
Brian R. Bondy

1
si. ma poi ha chiesto che l'operatore del caso + sia lento, qual è il modo migliore per fare una concatenazione. e qui entra in gioco l'operatore + =. ma sono d'accordo che la risposta di James sia un po 'breve. sembra che tutti potremmo usare l'operatore + ed è
estremamente

@ BrianR.Bondy operator+non deve restituire una nuova stringa. Gli implementatori possono restituire uno dei suoi operandi, modificato, se tale operando è stato passato per riferimento rvalue. libstdc++ fa questo, per esempio . Quindi, quando si chiama operator+con i provvisori, può ottenere le stesse prestazioni o quasi altrettanto buone, il che potrebbe essere un altro argomento a favore del default a meno che non si disponga di benchmark che dimostrino che rappresenta un collo di bottiglia.
underscore_d


4

In Imperfect C ++ , Matthew Wilson presenta un concatenatore di stringhe dinamico che precalcola la lunghezza della stringa finale in modo da avere una sola allocazione prima di concatenare tutte le parti. Possiamo anche implementare un concatenatore statico giocando con i modelli di espressione .

Questo tipo di idea è stato implementato nell'implementazione di STLport std :: string - che non è conforme allo standard a causa di questo preciso hack.


Glib::ustring::compose()dai collegamenti glibmm a GLib fa questo: stima reserve()es la lunghezza finale in base alla stringa di formato fornita e ai vararg, quindi append()s ciascuna (o la sua sostituzione formattata) in un ciclo. Immagino che questo sia un modo abbastanza comune di lavorare.
underscore_d

4

std::string operator+alloca una nuova stringa e copia ogni volta le due stringhe di operandi. ripetere molte volte e diventa costoso, O (n).

std::string appende operator+=d'altra parte, aumenta la capacità del 50% ogni volta che la corda deve crescere. Che riduce significativamente il numero di allocazioni di memoria e operazioni di copia, O (log n).


Non sono abbastanza sicuro del motivo per cui questo è stato downvoted. La cifra del 50% non è richiesta dallo Standard, ma l'IIRC o il 100% sono misure comuni di crescita nella pratica. Tutto il resto in questa risposta sembra ineccepibile.
underscore_d

Mesi dopo, suppongo che non sia così accurato, dal momento che è stato scritto molto tempo dopo il debutto di C ++ 11 e gli overload di operator+dove uno o entrambi gli argomenti vengono passati dal riferimento rvalue possono evitare di allocare una nuova stringa del tutto concatenandosi nel buffer esistente di uno degli operandi (anche se potrebbero dover riallocare se ha una capacità insufficiente).
underscore_d

2

Per piccole stringhe non importa. Se hai stringhe grandi, è meglio memorizzarle così come sono nel vettore o in qualche altra raccolta come parti. E addatta il tuo algoritmo per lavorare con questo insieme di dati invece che con un'unica grande stringa.

Preferisco std :: ostringstream per concatenazioni complesse.


2

Come con la maggior parte delle cose, è più facile non fare qualcosa che farlo.

Se vuoi inviare stringhe di grandi dimensioni a una GUI, è possibile che qualunque cosa tu stia trasmettendo possa gestire le stringhe in pezzi meglio di una stringa di grandi dimensioni (ad esempio, concatenando il testo in un editor di testo - di solito mantengono le righe separate strutture).

Se si desidera eseguire l'output su un file, eseguire lo streaming dei dati anziché creare una stringa di grandi dimensioni e inviarla in output.

Non ho mai riscontrato la necessità di rendere necessaria la concatenazione più veloce se ho rimosso la concatenazione non necessaria dal codice lento.


2

Probabilmente le migliori prestazioni se preallocate (riservate) spazio nella stringa risultante.

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

Uso:

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

0

Un semplice array di caratteri, incapsulato in una classe che tiene traccia della dimensione dell'array e del numero di byte allocati è il più veloce.

Il trucco è fare solo una grande allocazione all'inizio.

a

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

Punti di riferimenti

Per Visual Studio 2015, build di debug x86, miglioramento sostanziale rispetto a C ++ std :: string.

| API                   | Seconds           
| ----------------------|----| 
| SDS                   | 19 |  
| std::string           | 11 |  
| std::string (reserve) | 9  |  
| table_str_t           | 1  |  

1
L'OP è interessato a come concatenare in modo efficiente std::string. Non stanno chiedendo una classe stringa alternativa.
underscore_d

0

Puoi provare questo con le prenotazioni di memoria per ogni elemento:

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)), ...);
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.