web-dev-qa-db-fra.com

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

std::string_view est passé à C++ 17 et il est généralement recommandé de l'utiliser au lieu de _const std::string&_.

Une des raisons est la performance.

Quelqu'un peut-il expliquer comment exactement _std::string_view_ est/sera 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)

183
Patryk

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::string, et non dans un tableau C brut, un char const* renvoyé par une API C, un std::vector<char> produit par un moteur de désérialisation. , etc. La conversion de format évitée évite la copie d'octets et (si la chaîne est plus longue que le SBO¹ pour l'implémentation std::string 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 affectation n'est effectuée dans le cas string_view, mais il en serait ainsi si foo prenait un std::string const& au lieu d'un string_view.

La deuxième grande raison est que cela 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 dans std::string, chacun de ces nœuds d'analyse dans lequel ils stockent le nom ou la valeur d'un nœud copie les données d'origine du 2 chaîne gb à un nœud local.

Au lieu de cela, si vous l’analysez dans std::string_views, les nœuds font référence 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.

Il s'agit d'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 beaucoup, mais c'est quelque chose.

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

Si std::string_view avait un drapeau qui indiquait s'il était mis à zéro (ou quelque chose de plus sophistiqué), cela supprimerait même la dernière raison d'utiliser un std::string const&.

Dans certains cas, prendre un std::string sans const& est optimal par rapport à un std::string_view. Si vous devez posséder une copie de la chaîne indéfiniment après l'appel, la prise de valeur par 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 std::string local. Avoir deux surcharges std::string&& et std::string_view pourrait être plus rapide, mais seulement de manière marginale, et cela causerait une surcharge de code modeste (ce qui pourrait vous coûter tous les gains de vitesse).


¹ Optimisation du petit tampon

² Cas d'utilisation réel.

179

Une des manières dont string_view améliore les performances est qu’il permet de supprimer facilement les préfixes et les suffixes. Sous le capot, string_view peut simplement ajouter la taille du préfixe à un pointeur sur une chaîne de mémoire tampon, ou soustraire la taille du suffixe du compteur d'octets, ce processus est généralement rapide. Par contre, std :: string 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 la plupart des cas, vous voulez simplement obtenir une partie de la chaîne originale sans la 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");

Mise à jour:

J'ai écrit un repère très simple pour ajouter des nombres réels. J'ai utilisé génial bibliothèque de référence Google . 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

(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
52
Pavel Davydov

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 de nouveau tampon
  • vous pouvez prendre plusieurs tranches et sous-divisions dans un tampon existant sans allouer
  • la sous-chaîne est O (1), pas O (N)
  • ...

Meilleure et plus cohérente performance partout.


Le passage par valeur présente également des avantages par rapport au passage par référence, car le repliement du spectre.

Plus précisément, lorsque vous avez un paramètre std::string const&, rien ne garantit que la chaîne de référence ne sera pas modifiée. En conséquence, le compilateur doit extraire à nouveau le contenu de la chaîne après chaque appel dans une méthode opaque (pointeur sur les données, longueur, ...).

D'autre part, lors du passage d'un string_view par valeur, le compilateur peut déterminer de manière statique qu'aucun autre code ne peut modifier les pointeurs de longueur et de données actuellement sur la pile (ou dans les registres). En conséquence, il peut les "mettre en cache" lors d'appels de fonction.

43
Matthieu M.

Une chose à faire est d’éviter de construire un objet std::string dans le cas d’une conversion implicite à partir d’une chaîne terminée par un caractère nul:

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.
36
juanchopanza

std::string_view est 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&), parce que string* implique quelque chose comme:

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

Clairement, dans le but de passer des arguments const, le premier pointeur est superflu.

ps Une différence de Subancial entre std::string_view et const char*, est que les vues de chaîne ne sont pas obligatoirement terminées par zéro (elles ont une taille intégrée), ce qui permet pour l'épissage in-situ aléatoire de chaînes plus longues.

7
n.caillou