Comment exactement std :: ssortingng_view est-il plus rapide que const std :: ssortingng &?

std::ssortingng_view a std::ssortingng_view C ++ 17 et il est largement recommandé de l’utiliser à la place de const std::ssortingng& .

L’une des raisons est la performance.

Est-ce que quelqu’un peut expliquer comment exactement std::ssortingng_view est / sera plus rapide que const std::ssortingng& quand il est utilisé comme type de paramètre? (supposons qu’aucune copie dans l’appelé ne soit faite)

std::ssortingng_view est plus rapide dans quelques cas.

Tout d’abord, std::ssortingng const& exige que les données soient dans un std::ssortingng , et non un tableau C brut, un char const* renvoyé par une API C, un std::vector 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::ssortingng particulière) évite une allocation de mémoire.

 void foo( std::ssortingng_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 faite dans le cas de la ssortingng_view , mais il y aurait si foo prenait un std::ssortingng const& non une ssortingng_view .

La deuxième grande raison est que cela permet de travailler avec des sous-chaînes sans copie. Supposons que vous parsingz une chaîne json de 2 gigaoctets (!) ². Si vous l’parsing en std::ssortingng , chaque nœud d’parsing 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 std::ssortingng_view s, les nœuds se réfèrent aux données d’origine. Cela permet d’économiser des millions d’allocations et de réduire de moitié les besoins en mémoire lors de l’parsing.

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

Ceci est un cas extrême, mais d’autres cas “obtenir une sous-chaîne et travailler avec elle” peuvent également générer des accélérations décentes avec ssortingng_view .

Une partie importante de la décision est ce que vous perdez en utilisant std::ssortingng_view . Ce n’est pas beaucoup, mais c’est quelque chose.

Vous perdez implicitement la résiliation nulle, 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::ssortingng une fois peut être judicieuse. Ainsi, si vous savez que votre code nécessite un terminateur nul et que vous ne vous attendez pas à ce que les chaînes alimentées par des tampons de type C ou similaires prennent un std::ssortingng const& Sinon, prenez un std::ssortingng_view .

Si std::ssortingng_view avait un drapeau indiquant s’il était nul (ou quelque chose de plus intéressant), cela supprimerait même la dernière raison d’utiliser un std::ssortingng const& .

Il y a un cas où prendre un std::ssortingng sans const& est optimal sur un std::ssortingng_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 aucune allocation, juste quelques copies de caractères pour le dupliquer), ou vous pourrez déplacer le tampon alloué au tas dans une chaîne std::ssortingng locale. Avoir deux surcharges std::ssortingng&& et std::ssortingng_view pourrait être plus rapide, mais seulement de manière marginale, et provoquerait un léger gonflement du code (ce qui pourrait vous coûter tous les gains de vitesse).


¹ Optimisation du tampon petit

² Cas d’utilisation réel.

Une des manières dont ssortingng_view améliore les performances est qu’il permet de supprimer facilement les préfixes et les suffixes. Sous le capot, ssortingng_view peut simplement append la taille de préfixe à un pointeur vers un tampon de chaîne, ou soustraire la taille de suffixe du compteur d’octets, ce qui est généralement rapide. std :: ssortingng doit par contre 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 originale sans la copier). Exemple:

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

Avec std :: ssortingng_view:

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

Mettre à jour:

J’ai écrit un benchmark très simple pour append de vrais chiffres. J’ai utilisé une impressionnante bibliothèque de référence Google . Les fonctions de référence sont:

 ssortingng remove_prefix(const ssortingng &str) { return str.substr(3); } ssortingng_view remove_prefix(ssortingng_view str) { str.remove_prefix(3); return str; } static void BM_remove_prefix_ssortingng(benchmark::State& state) { std::ssortingng example{"asfaghdfgsghasfasg3423rfgasdg"}; while (state.KeepRunning()) { auto res = remove_prefix(example); // auto res = remove_prefix(ssortingng_view(example)); for ssortingng_view if (res != "aghdfgsghasfasg3423rfgasdg") { throw std::runtime_error("bad op"); } } } // BM_remove_prefix_ssortingng_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_ssortingng 90 ns 90 ns 7740626 BM_remove_prefix_ssortingng_view 6 ns 6 ns 120468514 

Il y a 2 raisons principales:

  • ssortingng_view est une tranche dans un tampon existant, il ne nécessite pas d’allocation de mémoire
  • ssortingng_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 constantes partout.


Passer par valeur présente également des avantages par rapport à la référence, car le crénelage.

Plus précisément, lorsque vous avez un paramètre std::ssortingng const& , 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 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’une ssortingng_view par valeur, le compilateur peut statiquement déterminer qu’aucun autre code ne peut modifier la longueur et les pointeurs de données maintenant sur la stack (ou dans les registres). Par conséquent, il peut les “mettre en cache” sur les appels de fonction.

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

 void foo(const std::ssortingng& s); ... foo("hello, world!"); // std::ssortingng object created, possible dynamic allocation. char msg[] = "good morning!"; foo(msg); // std::ssortingng object created, possible dynamic allocation. 

std::ssortingng_view est simplement 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 à la const ssortingng* (ou const ssortingng& ), car ssortingng* implique quelque chose comme:

 ssortingng* -> char* -> char[] | ssortingng | 

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

ps Une différence substantielle entre std::ssortingng_view et const char* , néanmoins, est que les ssortingng_views ne doivent pas nécessairement être terminés par une valeur null (ils ont une taille intégrée), ce qui permet l’épissage aléatoire des chaînes plus longues.