Concaténation de chaînes efficace en C ++

108

J'ai entendu quelques personnes exprimer des inquiétudes concernant l'opérateur "+" dans std :: string et diverses solutions de contournement pour accélérer la concaténation. Certains de ces éléments sont-ils vraiment nécessaires? Si tel est le cas, quelle est la meilleure façon de concaténer des chaînes en C ++?

ricaner
la source
13
Fondamentalement, le + n'est PAS un opérateur de concatentation (car il génère une nouvelle chaîne). Utilisez + = pour la concaténation.
Martin York
1
Depuis C ++ 11, il y a un point important: operator + peut modifier l'un de ses opérandes et le renvoyer par déplacement si cet opérande a été passé par référence rvalue. libstdc++ fait cela, par exemple . Ainsi, lors de l'appel de operator + avec des temporaires, il peut atteindre des performances presque aussi bonnes - peut-être un argument en faveur de son défaut, pour des raisons de lisibilité, à moins que l'on n'ait des repères montrant qu'il s'agit d'un goulot d'étranglement. Cependant, une variadique standardisée append()serait à la fois optimale et lisible ...
underscore_d

Réponses:

85

Le travail supplémentaire n'en vaut probablement pas la peine, sauf si vous avez vraiment besoin d'efficacité. Vous aurez probablement une bien meilleure efficacité simplement en utilisant l'opérateur + = à la place.

Maintenant, après cet avertissement, je vais répondre à votre question réelle ...

L'efficacité de la classe de chaînes STL dépend de l'implémentation de STL que vous utilisez.

Vous pouvez garantir l'efficacité et avoir un meilleur contrôle vous-même en effectuant la concaténation manuellement via les fonctions intégrées c.

Pourquoi operator + n'est pas efficace:

Jetez un œil à cette interface:

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)

Vous pouvez voir qu'un nouvel objet est renvoyé après chaque +. Cela signifie qu'un nouveau tampon est utilisé à chaque fois. Si vous faites une tonne d'opérations + supplémentaires, ce n'est pas efficace.

Pourquoi vous pouvez le rendre plus efficace:

  • Vous garantissez l'efficacité au lieu de faire confiance à un délégué pour le faire efficacement pour vous
  • la classe std :: string ne sait rien de la taille maximale de votre chaîne, ni de la fréquence à laquelle vous la concaténerez. Vous pouvez avoir ces connaissances et faire des choses en vous basant sur ces informations. Cela conduira à moins de réaffectations.
  • Vous contrôlerez les tampons manuellement afin d'être sûr de ne pas copier la chaîne entière dans de nouveaux tampons lorsque vous ne voulez pas que cela se produise.
  • Vous pouvez utiliser la pile pour vos tampons au lieu du tas qui est beaucoup plus efficace.
  • string + operator créera un nouvel objet string et le retournera donc en utilisant un nouveau tampon.

Considérations pour la mise en œuvre:

  • Gardez une trace de la longueur de la chaîne.
  • Gardez un pointeur vers la fin de la chaîne et le début, ou simplement le début et utilisez le début + la longueur comme décalage pour trouver la fin de la chaîne.
  • Assurez-vous que le tampon dans lequel vous stockez votre chaîne est suffisamment grand pour que vous n'ayez pas besoin de réallouer des données
  • Utilisez strcpy au lieu de strcat pour ne pas avoir besoin d'itérer sur la longueur de la chaîne pour trouver la fin de la chaîne.

Structure de données de corde:

Si vous avez besoin vraiment rapide concaténations envisager d' utiliser une structure de données de corde .

Brian R. Bondy
la source
6
Remarque: «STL» fait référence à une bibliothèque open-source complètement distincte, à l'origine par HP, dont une partie a été utilisée comme base pour des parties de la bibliothèque ISO Standard C ++. "std :: string", cependant, n'a jamais fait partie de la STL de HP, il est donc complètement faux de faire référence à "STL et" string "ensemble.
James Curran
1
Je ne dirais pas qu'il est faux d'utiliser STL et string ensemble. Voir sgi.com/tech/stl/table_of_contents.html
Brian R. Bondy
1
Lorsque SGI a pris en charge la maintenance de la STL de HP, elle a été rétro-adaptée pour correspondre à la bibliothèque standard (c'est pourquoi j'ai dit "ne fait jamais partie de la STL de HP"). Néanmoins, le créateur de std :: string est le comité ISO C ++.
James Curran
2
Note d'accompagnement: L'employé de SGI qui était en charge de la maintenance du STL pendant de nombreuses années était Matt Austern, qui, en même temps, dirigeait le sous-groupe Bibliothèque du comité de normalisation ISO C ++.
James Curran
4
Pouvez-vous s'il vous plaît clarifier ou expliquer pourquoi Vous pouvez utiliser la pile pour vos tampons au lieu du tas qui est beaucoup plus efficace. ? D'où vient cette différence d'efficacité?
h7r
76

Réservez votre dernier espace avant, puis utilisez la méthode append avec un tampon. Par exemple, disons que vous vous attendez à ce que la longueur de votre chaîne finale soit de 1 million de caractères:

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

while (whatever)
{
  s.append(buf,len);
}
Carlos A. Ibarra
la source
17

Je ne m'en soucierais pas. Si vous le faites en boucle, les chaînes préalloueront toujours la mémoire pour minimiser les réallocations - utilisez simplement operator+=dans ce cas. Et si vous le faites manuellement, quelque chose comme ça ou plus

a + " : " + c

Ensuite, il crée des temporaires - même si le compilateur peut éliminer certaines copies de valeur de retour. En effet, dans un appel successivement, operator+il ne sait pas si le paramètre de référence fait référence à un objet nommé ou à un temporaire renvoyé par un sous- operator+appel. Je préfère ne pas m'en soucier avant de ne pas avoir profilé au préalable. Mais prenons un exemple pour le montrer. Nous introduisons d'abord des parenthèses pour rendre la liaison claire. Je mets les arguments directement après la déclaration de fonction utilisée pour plus de clarté. En dessous, je montre quelle est alors l'expression résultante:

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

Maintenant, dans cet ajout, tmp1est ce qui a été renvoyé par le premier appel à operator + avec les arguments affichés. Nous supposons que le compilateur est vraiment intelligent et optimise la copie de la valeur de retour. Nous nous retrouvons donc avec une nouvelle chaîne contenant la concaténation de aet " : ". Maintenant, cela se produit:

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

Comparez cela à ce qui suit:

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

Il utilise la même fonction pour un temporaire et pour une chaîne nommée! Le compilateur doit donc copier l'argument dans une nouvelle chaîne et l'ajouter à cela et le renvoyer à partir du corps de operator+. Il ne peut pas prendre la mémoire d'un temporaire et y ajouter. Plus l'expression est grande, plus il faut faire de copies de chaînes.

Ensuite, Visual Studio et GCC prendront en charge la sémantique de déplacement de c ++ 1x (complétant la sémantique de copie ) et les références rvalue comme ajout expérimental. Cela permet de savoir si le paramètre fait référence à un temporaire ou non. Cela rendra ces ajouts incroyablement rapides, car tout ce qui précède se terminera dans un "add-pipeline" sans copies.

Si cela s'avère être un goulot d'étranglement, vous pouvez toujours le faire

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

Les appendappels ajoutent l'argument à *this, puis renvoient une référence à eux-mêmes. Il n'y a donc pas de copie des temporaires. Ou bien, le operator+=peut être utilisé, mais vous auriez besoin de parenthèses laides pour fixer la priorité.

Johannes Schaub - litb
la source
J'ai dû vérifier que les implémenteurs stdlib faisaient vraiment cela. : P libstdc++pour operator+(string const& lhs, string&& rhs)fait return std::move(rhs.insert(0, lhs)). Ensuite, si les deux sont temporaires, son operator+(string&& lhs, string&& rhs)si lhsa une capacité suffisante disponible sera juste directement append(). Là où je pense que cela risque d'être plus lent que operator+=si lhsn'a pas assez de capacité, comme alors il retombe rhs.insert(0, lhs), ce qui doit non seulement étendre le tampon et ajouter le nouveau contenu append(), mais doit également se déplacer le long du contenu d'origine de rhsright.
underscore_d
L'autre élément de surcharge par rapport à operator+=est que operator+doit toujours renvoyer une valeur, donc il doit à move()l'opérande auquel il est ajouté. Pourtant, je suppose que c'est une surcharge assez mineure (copier quelques pointeurs / tailles) par rapport à la copie profonde de la chaîne entière, donc c'est bien!
underscore_d
11

Pour la plupart des applications, cela n'a pas d'importance. Écrivez simplement votre code, ignorant parfaitement comment fonctionne l'opérateur +, et ne prenez les choses en main que si cela devient un goulot d'étranglement apparent.

Pesto
la source
7
Bien sûr, cela ne vaut pas la peine dans la plupart des cas, mais cela ne répond pas vraiment à sa question.
Brian R. Bondy
1
Ouais. je suis d'accord juste en disant "profil puis optimiser" peut être mis en commentaire sur la question :)
Johannes Schaub - litb
6
Techniquement, il a demandé si ceux-ci étaient «nécessaires». Ils ne le sont pas, et cela répond à cette question.
Samantha Branham
Assez juste, mais c'est vraiment nécessaire pour certaines applications. Donc, dans ces applications, la réponse se réduit à: `` prenez les choses en main ''
Brian R. Bondy
4
@Pesto Il y a une notion pervertie dans le monde de la programmation selon laquelle les performances n'ont pas d'importance et nous pouvons simplement ignorer toute l'affaire car les ordinateurs ne cessent de devenir plus rapides. Le fait est que ce n'est pas la raison pour laquelle les gens programment en C ++ et ce n'est pas pourquoi ils publient des questions sur le débordement de pile sur la concaténation efficace des chaînes.
MrFox
7

Contrairement à .NET System.Strings, les std :: strings de C ++ sont modifiables et peuvent donc être générées par simple concaténation tout aussi rapidement que par d'autres méthodes.

James Curran
la source
2
Surtout si vous utilisez reserve () pour rendre le tampon suffisamment grand pour le résultat avant de commencer.
Mark Ransom
je pense qu'il parle d'opérateur + =. c'est aussi concaténant, bien que ce soit un cas dégénéré. james était un vc ++ mvp donc je suppose qu'il a une idée de c ++: p
Johannes Schaub - litb
1
Je ne doute pas une seconde qu'il ait une connaissance approfondie du C ++, juste qu'il y ait eu un malentendu sur la question. La question posée sur l'efficacité de operator + qui renvoie de nouveaux objets string à chaque fois qu'il est appelé, et utilise donc de nouveaux tampons char.
Brian R. Bondy
1
Ouais. mais ensuite il a demandé l'opérateur de cas + est lent, quel est le meilleur moyen de faire une concaténation. et ici l'opérateur + = entre en jeu. mais je suis d'accord que la réponse de James est un peu courte. cela donne l'impression que nous pourrions tous utiliser operator + et c'est très efficace: p
Johannes Schaub - litb
@ BrianR.Bondy operator+n'a pas besoin de renvoyer une nouvelle chaîne. Les implémenteurs peuvent renvoyer l'un de ses opérandes, modifié, si cet opérande a été passé par référence rvalue. libstdc++ fait cela, par exemple . Ainsi, lors d'un appel operator+avec des temporaires, il peut obtenir des performances identiques ou presque aussi bonnes - ce qui pourrait être un autre argument en faveur de son défaut, à moins que l'on n'ait des repères montrant qu'il représente un goulot d'étranglement.
underscore_d
4

En C ++ imparfait , Matthew Wilson présente un concaténateur de chaîne dynamique qui précalcule la longueur de la chaîne finale afin de n'avoir qu'une seule allocation avant de concaténer toutes les parties. Nous pouvons également implémenter un concaténateur statique en jouant avec des modèles d'expression .

Ce genre d'idée a été implémenté dans l'implémentation STLport std :: string - qui n'est pas conforme à la norme à cause de ce hack précis.

Luc Hermitte
la source
Glib::ustring::compose()des liaisons glibmm à GLib fait cela: estime et reserve()s la longueur finale basée sur la chaîne de format fournie et les varargs, puis append()s chacun (ou son remplacement formaté) dans une boucle. Je pense que c'est une façon de travailler assez courante.
underscore_d
4

std::string operator+alloue une nouvelle chaîne et copie les deux chaînes d'opérande à chaque fois. répétez plusieurs fois et cela devient cher, O (n).

std::string appendet operator+=d'autre part, augmentez la capacité de 50% à chaque fois que la chaîne doit grossir. Ce qui réduit considérablement le nombre d'allocations de mémoire et d'opérations de copie, O (log n).

Timmerov
la source
Je ne sais pas très bien pourquoi cela a été rejeté. Le chiffre de 50% n'est pas exigé par la norme, mais l'IIRC ou 100% sont des mesures courantes de la croissance dans la pratique. Tout le reste dans cette réponse semble irréfutable.
underscore_d
Des mois plus tard, je suppose que ce n'est pas si précis, car il a été écrit longtemps après le lancement de C ++ 11, et les surcharges de l' operator+endroit où un ou les deux arguments sont passés par référence à rvalue peuvent éviter d'allouer une nouvelle chaîne en concaténant dans le tampon existant de l'un des opérandes (même s'ils devront peut-être réallouer si sa capacité est insuffisante).
underscore_d
2

Pour les petites cordes, cela n'a pas d'importance. Si vous avez de grosses chaînes, vous feriez mieux de les stocker telles qu'elles sont en vecteur ou dans une autre collection en tant que parties. Et adaptez votre algorithme pour travailler avec un tel ensemble de données au lieu d'une seule grande chaîne.

Je préfère std :: ostringstream pour la concaténation complexe.

Mykola Golubyev
la source
2

Comme pour la plupart des choses, il est plus facile de ne pas faire quelque chose que de le faire.

Si vous souhaitez afficher de grandes chaînes vers une interface graphique, il se peut que tout ce que vous produisez puisse gérer les chaînes en morceaux mieux que comme une grande chaîne (par exemple, concaténer du texte dans un éditeur de texte - généralement, ils gardent les lignes séparées structures).

Si vous souhaitez générer une sortie dans un fichier, diffusez les données plutôt que de créer une grande chaîne et de la générer.

Je n'ai jamais trouvé le besoin de rendre la concaténation plus rapide si je supprimais la concaténation inutile du code lent.

Pete Kirkham
la source
2

Probablement meilleures performances si vous pré-allouez (réservez) de l'espace dans la chaîne résultante.

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

Usage:

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

Un simple tableau de caractères, encapsulé dans une classe qui garde la trace de la taille du tableau et du nombre d'octets alloués, est le plus rapide.

L'astuce consiste à ne faire qu'une seule grande allocation au départ.

à

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

Benchmarks

Pour Visual Studio 2015, build de débogage x86, amélioration substantielle par rapport à C ++ std :: string.

| API                   | Seconds           
| ----------------------|----| 
| SDS                   | 19 |  
| std::string           | 11 |  
| std::string (reserve) | 9  |  
| table_str_t           | 1  |  
Pedro Vicente
la source
1
Le PO s'intéresse à la manière de concaténer efficacement std::string. Ils ne demandent pas une classe de chaînes alternative.
underscore_d
0

Vous pouvez essayer celui-ci avec des réservations de mémoire pour chaque élément:

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)), ...);
}
Voltento
la source