Comment std :: string_view est-il plus rapide que const std :: string &?

221

std::string_viewl'a fait en C ++ 17 et il est largement recommandé de l'utiliser à la place de const std::string&.

L'une des raisons est la performance.

Quelqu'un peut-il expliquer comment est / sera exactement std::string_view plus rapide que const std::string&lorsqu'il est utilisé comme type de paramètre? (supposons qu'aucune copie dans l'appelé ne soit faite)

Patryk
la source
7
std::string_viewest juste une abstraction de la paire (char * begin, char * end). Vous l'utilisez lorsque vous créez une std::stringcopie inutile.
QuestionC
À mon avis, la question n'est pas exactement laquelle est la plus rapide, mais quand les utiliser. Si j'ai besoin d'une manipulation sur la chaîne et qu'elle n'est pas permanente et / ou que je garde la valeur d'origine, string_view est parfait car je n'ai pas besoin d'y faire une copie de la chaîne. Mais si j'ai seulement besoin de vérifier quelque chose sur une chaîne en utilisant string :: find par exemple, alors la référence est meilleure.
TheArquitect
@QuestionC vous utilisez quand vous ne voulez pas que votre API pour limiter à std::string(string_view peut accepter que des tableaux bruts, des vecteurs, std::basic_string<>avec allocateurs non par défaut , etc. , etc. , etc. Oh, et d' autres string_views évidemment)
sehe

Réponses:

213

std::string_view est plus rapide dans quelques cas.

Tout d'abord, std::string const&nécessite que les données soient dans un std::stringtableau C, et non dans un tableau C brut, char const*renvoyé par une API C, std::vector<char>produit par un moteur de désérialisation, etc. La conversion de format évitée évite de copier des octets, et (si la chaîne est plus longue que le SBO¹ pour l' std::stringimplémentation particulière ) évite une allocation de mémoire.

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

Aucune allocation n'est effectuée dans le string_viewcas, mais il y aurait si foopris un std::string const&au lieu d'un string_view.

La deuxième raison très importante est qu'elle permet de travailler avec des sous-chaînes sans copie. Supposons que vous analysez une chaîne json de 2 gigaoctets (!) ². Si vous l'analysez std::string, chacun de ces nœuds d'analyse où ils stockent le nom ou la valeur d'un nœud copie les données d'origine de la chaîne de 2 Go vers un nœud local.

Au lieu de cela, si vous l'analysez en std::string_views, les nœuds se réfèrent aux données d'origine. Cela peut économiser des millions d'allocations et réduire de moitié les besoins en mémoire lors de l'analyse.

L'accélération que vous pouvez obtenir est tout simplement ridicule.

C'est un cas extrême, mais d'autres cas «obtenez une sous-chaîne et travaillez avec elle» peuvent également générer des accélérations décentes avec string_view.

Une partie importante de la décision est ce que vous perdez en utilisant std::string_view. Ce n'est pas grand-chose, mais c'est quelque chose.

Vous perdez la terminaison nulle implicite, et c'est à peu près tout. Donc, si la même chaîne est passée à 3 fonctions qui nécessitent toutes un terminateur nul, la conversion en std::stringune fois peut être judicieuse. Ainsi, si votre code est connu pour avoir besoin d'un terminateur nul et que vous ne vous attendez pas à ce que des chaînes alimentées à partir de tampons de type C ou similaires, prenez peut-être un std::string const&. Sinon, prenez un std::string_view.

Si std::string_viewun indicateur indiquait s'il était terminé (ou quelque chose de plus sophistiqué), il supprimerait même cette dernière raison d'utiliser a std::string const&.

Il y a un cas où prendre un std::stringavec non const&est optimal par rapport à a std::string_view. Si vous devez posséder une copie de la chaîne indéfiniment après l'appel, la prise de valeur est efficace. Vous serez soit dans le cas SBO (et pas d'allocations, juste quelques copies de caractères pour le dupliquer), ou vous pourrez déplacer le tampon alloué par tas dans un local std::string. Avoir deux surcharges std::string&&et std::string_viewpeut être plus rapide, mais seulement de manière marginale, et cela entraînerait une légère surcharge de code (ce qui pourrait vous coûter tous les gains de vitesse).


¹ Optimisation des petits tampons

² Cas d'utilisation réel.

Yakk - Adam Nevraumont
la source
8
Vous perdez également la propriété. Ce qui n'est intéressant que si la chaîne est renvoyée et il pourrait être autre chose qu'une sous-chaîne d'un tampon qui est garantie de survivre assez longtemps. En fait, la perte de propriété est une arme à double tranchant.
Déduplicateur
SBO semble étrange. J'ai toujours entendu SSO (optimisation des petites chaînes)
phuclv
@phu Sure; mais les chaînes ne sont pas la seule chose sur laquelle vous utilisez l'astuce.
Yakk - Adam Nevraumont
@phuclv SSO n'est qu'un cas spécifique de SBO, qui signifie petite optimisation de tampon . Les termes alternatifs sont les petites données opt. , petit objet opt. , ou petite taille opt. .
Daniel Langr
59

Une façon dont string_view améliore les performances est qu'il permet de supprimer facilement les préfixes et suffixes. Sous le capot, string_view peut simplement ajouter la taille du préfixe à un pointeur dans un tampon de chaîne, ou soustraire la taille du suffixe du compteur d'octets, c'est généralement rapide. std :: string d'autre part doit copier ses octets lorsque vous faites quelque chose comme substr (de cette façon, vous obtenez une nouvelle chaîne qui possède son tampon, mais dans de nombreux cas, vous voulez simplement obtenir une partie de la chaîne d'origine sans copier). Exemple:

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

Avec std :: string_view:

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

Mettre à jour:

J'ai écrit un point de repère très simple pour ajouter des nombres réels. J'ai utilisé la bibliothèque de référence Google génial . Les fonctions référencées sont:

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

Résultats

(Linux x86_64, 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
la source
2
C'est formidable que vous ayez fourni une référence réelle. Cela montre vraiment ce qui peut être gagné dans les cas d'utilisation pertinents.
Daniel Kamil Kozar
1
@DanielKamilKozar Merci pour la rétroaction. Je pense aussi que les repères sont précieux, parfois ils changent tout.
Pavel Davydov
47

Il y a 2 raisons principales:

  • string_view est une tranche dans un tampon existant, elle ne nécessite pas d'allocation de mémoire
  • string_view est passé par valeur, pas par référence

Les avantages d'avoir une tranche sont multiples:

  • vous pouvez l'utiliser avec char const*ou char[]sans allouer un nouveau tampon
  • vous pouvez prendre plusieurs tranches et sous-tranches dans un tampon existant sans allouer
  • la sous-chaîne est O (1), pas O (N)
  • ...

Des performances meilleures et plus cohérentes partout.


Le passage par valeur présente également des avantages par rapport au passage par référence, car l'aliasing.

Plus précisément, lorsque vous avez un std::string const& paramètre, il n'y a aucune garantie que la chaîne de référence ne sera pas modifiée. Par conséquent, le compilateur doit récupérer à nouveau le contenu de la chaîne après chaque appel dans une méthode opaque (pointeur sur les données, longueur, ...).

D'un autre côté, lors du passage d'une string_viewvaleur par, le compilateur peut déterminer statiquement qu'aucun autre code ne peut modifier la longueur et les pointeurs de données maintenant sur la pile (ou dans les registres). Par conséquent, il peut les "mettre en cache" sur les appels de fonction.

Matthieu M.
la source
36

Une chose qu'il peut faire est d'éviter de construire un std::stringobjet dans le cas d'une conversion implicite à partir d'une chaîne terminée par 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.
juanchopanza
la source
12
Il vaut peut-être la peine de dire que ce ne seraconst std::string str{"goodbye!"}; foo(str); probablement pas plus rapide avec string_view qu'avec string &
Martin Bonner prend en charge Monica
1
Ne string_viewsera pas lent car il doit copier deux pointeurs au lieu d'un pointeur const string&?
balki
9

std::string_viewest fondamentalement juste un wrapper autour d'un const char*. Et passer const char*signifie qu'il y aura un pointeur de moins dans le système par rapport à passer const string*(ou const string&), car cela string*implique quelque chose comme:

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

De toute évidence, dans le but de passer des arguments const, le premier pointeur est superflu.

ps Une différence substantielle entre std::string_viewet const char*, néanmoins, est que les string_views ne doivent pas être terminées par un caractère nul (elles ont une taille intégrée), et cela permet un épissage aléatoire sur place de chaînes plus longues.

n.caillou
la source
4
Qu'est-ce que les downvotes? std::string_views sont juste fantaisistes const char*, point final. GCC les implémente comme ceci:class basic_string_view {const _CharT* _M_str; size_t _M_len;}
n.caillou
4
il suffit de passer à 65K rep (à partir de vos 65 actuels) et ce serait la réponse acceptée (vagues à la foule culte) :)
mlvljr
7
@mlvljr Personne ne passe std::string const*. Et ce diagramme est inintelligible. @ n.caillou: Votre propre commentaire est déjà plus précis que la réponse. Cela fait string_viewplus que "fantaisie char const*" - c'est vraiment assez évident.
sehe
@sehe je pourrais être que personne, pas de problème (c'est-à-dire passer un pointeur (ou une référence) à une chaîne de const, pourquoi pas?) :)
mlvljr
2
@sehe Vous comprenez cela du point de vue de l'optimisation ou de l'exécution, std::string const*et std::string const&êtes les mêmes, n'est-ce pas?
n.caillou