In che modo esattamente std :: string_view è più veloce di const std :: string &?


221

std::string_viewè arrivato a C ++ 17 ed è ampiamente raccomandato di usarlo al posto di const std::string&.

Uno dei motivi è la prestazione.

Qualcuno può spiegare come è / sarà esattamente std::string_view più veloce di const std::string&quando usato come tipo di parametro? (supponiamo che non vengano fatte copie nella chiamata)


7
std::string_viewè solo un'astrazione della coppia (char * begin, char * end). Lo usi quando fai una std::stringsarebbe una copia non necessaria.
Domanda

Secondo me la domanda non è esattamente quale sia più veloce, ma quando usarli. Se ho bisogno di manipolazioni sulla stringa e non è permanente e / o mantieni il valore originale, string_view è perfetto perché non ho bisogno di farne una copia. Ma se ho solo bisogno di controllare qualcosa sulla stringa usando string :: find per esempio, allora il riferimento è migliore.
TheArquitect il

@QuestionC la usi quando non vuoi che la tua API si limiti a std::string(string_view può accettare matrici, vettori, std::basic_string<>allocatori non predefiniti ecc. Ecc. Ecc. Ecc. Oh, e altre string_view ovviamente)
vedi il

Risposte:


213

std::string_view è più veloce in alcuni casi.

Innanzitutto, std::string const&richiede che i dati si trovino in un std::stringarray C, e non in un raw, char const*restituiti da un'API C, std::vector<char>prodotti da un motore di deserializzazione, ecc. La conversione del formato evitata evita di copiare byte e (se la stringa è più lunga del SBO¹ per l' std::stringimplementazione particolare ) evita un'allocazione di memoria.

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

Nessuna allocazione viene eseguita nel string_viewcaso, ma ci sarebbe se foopreso un std::string const&invece di un string_view.

La seconda grande ragione è che consente di lavorare con sottostringhe senza una copia. Supponiamo che stai analizzando una stringa json da 2 gigabyte (!) ². Se lo analizzi std::string, ciascuno di questi nodi di analisi in cui memorizzano il nome o il valore di un nodo copia i dati originali dalla stringa da 2 GB in un nodo locale.

Invece, se lo analizzi in std::string_views, i nodi si riferiscono ai dati originali. Ciò può salvare milioni di allocazioni e dimezzare i requisiti di memoria durante l'analisi.

La velocità che puoi ottenere è semplicemente ridicola.

Questo è un caso estremo, ma altri casi "procurati una sottostringa e lavoraci" possono anche generare velocizzazioni decenti string_view.

Una parte importante della decisione è ciò che si perde utilizzando std::string_view. Non è molto, ma è qualcosa.

Si perde la terminazione nulla implicita, e questo è tutto. Quindi, se la stessa stringa verrà passata a 3 funzioni che richiedono tutte un terminatore nullo, la conversione in std::stringuna volta potrebbe essere saggia. Pertanto, se è noto che il tuo codice necessita di un terminatore nullo e non ti aspetti stringhe alimentate da buffer di origine C o simili, forse prendi a std::string const&. Altrimenti prendi a std::string_view.

Se std::string_viewavesse una bandiera che indicava se era terminata con null (o qualcosa di più elaborato), rimuoverebbe anche l'ultimo motivo per usare a std::string const&.

C'è un caso in cui prendere a std::stringcon no const&è ottimale su a std::string_view. Se è necessario possedere una copia della stringa a tempo indeterminato dopo la chiamata, la presa per valore è efficiente. O sarai nel caso SBO (e nessuna allocazione, solo poche copie di caratteri per duplicarlo), oppure sarai in grado di spostare il buffer allocato in heap in un locale std::string. Avere due sovraccarichi std::string&&e std::string_viewpotrebbe essere più veloce, ma solo marginalmente, e causerebbe un gonfio di codice modesto (che potrebbe costare tutti i guadagni di velocità).


¹ Ottimizzazione buffer di piccole dimensioni

² Caso d'uso reale.


8
Si perde anche la proprietà. Il che è interessante solo se la stringa viene restituita e potrebbe essere necessario qualcosa di diverso da una sottostringa di un buffer che è garantito per sopravvivere abbastanza a lungo. In realtà, la perdita di proprietà è un'arma a doppio taglio.
Deduplicatore

SBO sembra strano. Ho sempre sentito SSO (ottimizzazione delle stringhe di piccole dimensioni)
phuclv

@phu Certo; ma le stringhe non sono l'unica cosa su cui usi il trucco.
Yakk - Adam Nevraumont,

@phuclv SSO è solo un caso specifico di SBO, che sta per ottimizzazione del buffer di piccole dimensioni . I termini alternativi sono piccoli dati opt. , piccolo oggetto opt. o optare per dimensioni ridotte. .
Daniel Langr,

59

Un modo in cui string_view migliora le prestazioni è che consente di rimuovere facilmente prefissi e suffissi. Sotto il cofano, string_view può semplicemente aggiungere la dimensione del prefisso a un puntatore a un buffer di stringa o sottrarre la dimensione del suffisso dal contatore di byte, di solito è veloce. std :: string, d'altra parte, deve copiare i suoi byte quando si fa qualcosa come substr (in questo modo si ottiene una nuova stringa che possiede il suo buffer, ma in molti casi si desidera solo ottenere parte della stringa originale senza copiare). Esempio:

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

Con std :: string_view:

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

Aggiornare:

Ho scritto un benchmark molto semplice per aggiungere alcuni numeri reali. Ho usato una fantastica libreria di benchmark di Google . Le funzioni di benchmark sono:

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

risultati

(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

2
È fantastico che tu abbia fornito un benchmark reale. Questo dimostra davvero cosa si può ottenere in casi d'uso rilevanti.
Daniel Kamil Kozar,

1
@DanielKamilKozar Grazie per il feedback. Penso anche che i benchmark siano preziosi, a volte cambiano tutto.
Pavel Davydov,

47

Ci sono 2 motivi principali:

  • string_view è una sezione in un buffer esistente, non richiede un'allocazione di memoria
  • string_view viene passato per valore, non per riferimento

I vantaggi di avere una fetta sono molteplici:

  • puoi usarlo con char const*o char[]senza allocare un nuovo buffer
  • puoi prendere più slice e sottotitoli in un buffer esistente senza allocarlo
  • la sottostringa è O (1), non O (N)
  • ...

Prestazioni migliori e più coerenti dappertutto.


Il passaggio per valore ha anche dei vantaggi rispetto al passaggio per riferimento, in quanto l'aliasing.

In particolare, quando si dispone di un std::string const&parametro, non si garantisce che la stringa di riferimento non verrà modificata. Di conseguenza, il compilatore deve recuperare nuovamente il contenuto della stringa dopo ogni chiamata in un metodo opaco (puntatore a dati, lunghezza, ...).

D'altra parte, quando passa un string_viewvalore, il compilatore può determinare staticamente che nessun altro codice può modificare la lunghezza e i puntatori di dati ora nello stack (o nei registri). Di conseguenza, può "memorizzarli nella cache" tra le chiamate di funzione.


36

Una cosa che può fare è evitare di costruire un std::stringoggetto nel caso di una conversione implicita da una stringa terminata null:

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.

12
Vale la pena dire che const std::string str{"goodbye!"}; foo(str);probabilmente non sarà più veloce con string_view che con string &
Martin Bonner supporta Monica il

1
Non string_viewsarà lento in quanto deve copiare due puntatori anziché un puntatore in const string&?
Balki,

9

std::string_viewè fondamentalmente solo un involucro attorno a const char*. E passare const char*significa che ci sarà un puntatore in meno nel sistema rispetto al passaggio const string*(o const string&), perché string*implica qualcosa come:

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

Chiaramente ai fini del passaggio di argomenti const, il primo puntatore è superfluo.

ps Una differenza sostanziale tra std::string_viewe const char*, tuttavia, è che le string_views non devono essere terminate con null (hanno dimensioni incorporate), e ciò consente lo splicing sul posto casuale di stringhe più lunghe.


4
Cosa sono i downvotes? std::string_viewsono solo fantasie const char*, punto. GCC li implementa in questo modo:class basic_string_view {const _CharT* _M_str; size_t _M_len;}
n.caillou,

4
basta arrivare a un rappresentante di 65.000 (dai tuoi attuali 65) e questa sarebbe la risposta accettata (
saluta le

7
@mlvljr Nessuno passa std::string const*. E quel diagramma è incomprensibile. @ n.caillou: il tuo commento è già più preciso della risposta. Questo rende string_viewpiù che "fantasioso char const*" - è davvero abbastanza ovvio.
guarda il

@sehe Potrei essere che nessuno, nessun problemo (cioè passare un puntatore (o riferimento) a una stringa const, perché no?) :)
mlvljr

2
@sehe Lo capisci dal punto di vista dell'ottimizzazione o dell'esecuzione std::string const*e std::string const&sei lo stesso, vero?
n.caillou,
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.