Est-il inefficace de concaténer des chaînes une à la fois?

11

Je me souviens de mes jours de programmation en C que lorsque deux chaînes sont jointes, le système d'exploitation doit allouer de la mémoire pour la chaîne jointe, puis le programme peut copier tout le texte de la chaîne dans la nouvelle zone en mémoire, puis l'ancienne mémoire doit manuellement être libéré. Donc, si cela se fait plusieurs fois comme dans le cas de la jonction d'une liste, le système d'exploitation doit constamment allouer de plus en plus de mémoire, juste pour qu'il soit libéré après la prochaine concaténation. Une bien meilleure façon de le faire en C serait de déterminer la taille totale des chaînes combinées et d'allouer la mémoire nécessaire pour toute la liste de chaînes jointes.

Maintenant, dans les langages de programmation modernes (C # par exemple), je vois souvent le contenu des collections réunies en itérant dans la collection et en ajoutant toutes les chaînes, une à la fois, à une seule référence de chaîne. N'est-ce pas inefficace, même avec une puissance de calcul moderne?

JSideris
la source
laissez-le au compilateur et au profileur, ils s'en soucieront, votre temps sera beaucoup plus cher que celui de la concaténation de chaînes.
OZ_
7
Cela dépend de l'implémentation - vous devriez vraiment vérifier la documentation de votre bibliothèque de chaînes particulière. Il est possible d'implémenter des chaînes qui concaténent par référence, en temps O (1). Dans tous les cas, si vous devez concaténer une liste de chaînes arbitrairement longue, vous devez utiliser des classes ou des fonctions conçues pour ce genre de chose.
comingstorm
Notez que des choses comme la concaténation de chaînes sont généralement gérées par une fonction de bibliothèque, pas par le système d'exploitation. Le système d'exploitation peut être impliqué dans l'allocation de mémoire, mais probablement pas pour les objets relativement petits comme les chaînes.
Caleb
@Caleb L'OS est impliqué dans TOUTES les allocations de mémoire. Ne pas suivre cette règle est un type de fuite de mémoire. L'exception est lorsque vous avez des chaînes codées en dur dans l'application; ceux-ci sont écrits sous forme de données binaires dans l'assembly généré. Mais dès que vous manipulez (ou peut-être même attribuez) une chaîne, elle doit être stockée en mémoire (c'est-à-dire que la mémoire doit être allouée).
JSideris
4
@Bizorke Dans un scénario typique, un allocateur de mémoire comme malloc () (qui fait partie de la bibliothèque standard C, pas le système d'exploitation) est utilisé pour allouer différents morceaux de mémoire à partir de la mémoire qui a déjà été allouée au processus par le système d'exploitation. Le système d'exploitation n'a pas besoin de s'impliquer sauf si le processus manque de mémoire et doit en demander plus. Il peut également participer à un niveau inférieur si une allocation provoque un défaut de page. Donc oui, le système d'exploitation fournit finalement la mémoire, mais il n'est pas nécessairement impliqué dans l'allocation fragmentaire des chaînes et d'autres objets à l'intérieur du processus.
Caleb

Réponses:

21

Votre explication de la raison pour laquelle elle est inefficace est exacte, dans au moins les langages que je connais (C, Java, C #), bien que je ne pense pas qu'il soit universellement courant d'effectuer des quantités massives de concaténation de chaînes. Dans le code C # Je travaille, il y a l' utilisation abondante de StringBuilder, String.Format, etc. , qui sont tous techiniques sauver la mémoire pour éviter une réaffectation.

Donc, pour obtenir la réponse à votre question, nous devons poser une autre question: s'il n'est jamais vraiment un problème de concaténer des chaînes, pourquoi les classes aimeraient-elles StringBuilderet StringBufferexisteraient-elles ? Pourquoi l'utilisation de telles classes est-elle incluse dans les livres et les classes de programmation même semi-débutants? Pourquoi les conseils d'optimisation apparemment prématurés seraient-ils si importants?

Si la plupart des développeurs de concaténation de chaînes fondaient leur réponse uniquement sur l'expérience, la plupart diraient que cela ne fait aucune différence et éviteraient l'utilisation de tels outils en faveur du "plus lisible" for (int i=0; i<1000; i++) { strA += strB; }. Mais ils ne l'ont jamais mesuré.

La vraie réponse à cette question pourrait être trouvée dans cette réponse SO , qui révèle que dans un cas, lors de la concaténation de 50000 chaînes (qui, selon votre application, peut être un phénomène courant), même les plus petites, ont abouti à un résultat de performance de 1000x .

Si la performance ne signifie littéralement rien, concaténer loin. Mais je ne serais pas d'accord que l'utilisation d'alternatives (StringBuilder) est difficile ou moins lisible , et serait donc une pratique de programmation raisonnable qui ne devrait pas invoquer la défense "d'optimisation prématurée".

METTRE À JOUR:

Je pense que cela revient à connaître votre plate-forme et à suivre ses meilleures pratiques, qui ne sont malheureusement pas universelles . Deux exemples de deux "langues modernes" différentes:

  1. Dans une autre réponse SO , les caractéristiques de performance opposées exactes (array.join vs + =) se sont avérées parfois vraies en JavaScript . Dans certains navigateurs, la concaténation de chaînes semble être optimisée automatiquement, et dans d'autres cas, elle ne l'est pas. Donc la recommandation (au moins dans cette question SO), est de simplement concaténer et de ne pas s'en inquiéter.
  2. Dans un autre cas, un compilateur Java peut remplacer automatiquement la concaténation par une construction plus efficace telle que StringBuilder. Cependant, comme d'autres l'ont souligné, cela est indéterministe, non garanti, et l'utilisation de StringBuilder ne nuit pas à la lisibilité. Dans ce cas particulier, j'ai tendance à déconseiller l'utilisation de la concaténation pour les grandes collections ou le recours à un comportement de compilateur Java indéterministe. De même, dans .NET, aucune optimisation du tri n'est jamais effectuée.

Ce n'est pas exactement un péché cardinal de ne pas connaître immédiatement toutes les nuances de chaque plate-forme, mais ignorer des problèmes de plate-forme importants comme celui-ci reviendrait presque à passer de Java à C ++ et à ne pas se soucier de désallouer la mémoire.

Kevin McCormick
la source
-1: contient des BS majeurs. strA + strBest exactement la même chose que l'utilisation d'un StringBuilder. Il a un hit de performance 1x. Ou 0x, selon la façon dont vous mesurez. Pour plus de détails, codinghorror.com/blog/2009/01/…
amara
5
@sparkleshy: Je suppose que la réponse SO utilise Java et que votre article lié utilise C #. Je suis d'accord avec ceux qui disent "dépend de la mise en œuvre" et "mesurez-le pour votre environnement particulier".
Kai Chan
1
@KaiChan: la concaténation de chaînes est fondamentalement la même en java et en c #
amara
3
@sparkleshy - Point pris, mais utiliser StringBuilder, String.Join, etc. pour concaténer exactement deux chaînes est rarement une recommandation, jamais. De plus, la question du PO concerne spécifiquement "le contenu des collections réunies", ce qui n'est pas le cas (où StringBuilder, etc. est très applicable). Quoi qu'il en soit, je mettrai à jour mon exemple pour être plus précis.
Kevin McCormick
3
Je ne me soucie pas de la langue aux fins de cette question. L'utilisation de stringbuilder dans les coulisses dans certaines langues explique pourquoi il peut ne pas être inefficace de concaténer une liste entière de chaînes, ce qui répond à ma question. Cette réponse expliquait cependant que rejoindre une liste pouvait être potentiellement dangereux et recommandait le constructeur de chaînes comme alternative. Je recommande d'ajouter l'utilisation du compilateur de stringbuilder dans les coulisses à votre réponse, afin d'éviter une perte de réputation ou une mauvaise interprétation.
JSideris
2

Ce n'est pas efficace, à peu près pour les raisons que vous avez décrites. Les chaînes en C # et Java sont immuables. Les opérations sur les chaînes retournent une instance distincte au lieu de modifier l'original, contrairement à ce qui se passait en C. Lors de la concaténation de plusieurs chaînes, une instance distincte est créée à chaque étape. L'allocation et la récupération ultérieure de la mémoire de ces instances inutilisées peuvent entraîner une baisse des performances. Seule cette gestion de la mémoire de temps est gérée pour vous par le garbage collector.

C # et Java introduisent une classe StringBuilder en tant que chaîne mutable spécifiquement pour ce type de tâches. Un équivalent en C serait d'utiliser une liste chaînée de chaînes concaténées au lieu de les joindre dans un tableau. C # propose également une méthode Join pratique sur les chaînes pour joindre une collection de chaînes.

scrwtp
la source
1

À strictement parler, il s'agit d'une utilisation moins efficace des cycles du processeur, vous avez donc raison. Mais qu'en est-il du temps du développeur, des coûts de maintenance, etc. Si vous ajoutez le coût du temps à l'équation, il est presque toujours plus efficace de faire ce qui est le plus facile, puis si nécessaire, profilez et optimisez les bits lents.
"La première règle d'optimisation de programme: ne le faites pas. La deuxième règle d'optimisation de programme (pour les experts seulement!): Ne le faites pas encore."

mattnz
la source
3
règles pas très efficaces, je pense.
OZ_
@OZ_: Ceci est une citation largement utilisée (Michael A. Jackson) et d'autres par des goûts de Donald Knuth ... Ensuite, il y a celui-ci, que je m'abstiens généralement d'utiliser "Plus de péchés informatiques sont commis au nom de l'efficacité ( sans nécessairement y parvenir) que pour toute autre raison - y compris la stupidité aveugle. "
mattnz
2
Je dois souligner que Michael A. Jackson était britannique, donc c'est l' optimisation et non l' optimisation . À un moment donné, je devrais vraiment corriger la page wikipedia . * 8 ')
Mark Booth
Je suis totalement d'accord, vous devez corriger ces fautes d'orthographe. Bien que ma langue maternelle soit l'anglais Queens, je trouve plus facile de parler américain sur le Web .......
mattnz
personne ne pense aux utilisateurs. Vous pouvez accélérer légèrement la création du développeur, mais alors chacun de vos clients en souffre. Écrivez votre code pour eux, pas pour vous.
gbjbaanb
1

Il est très difficile de dire quoi que ce soit sur les performances sans un test pratique. Récemment, j'ai été très surpris de découvrir qu'en JavaScript, une concaténation de chaînes naïve était généralement plus rapide que la solution recommandée "make list and join" (testez ici , comparez t1 à t4). Je suis toujours perplexe quant à la raison pour laquelle cela se produit.

Voici quelques questions que vous pourriez vous poser lorsque vous raisonnez sur les performances (en particulier en ce qui concerne l'utilisation de la mémoire): 1) quelle est la taille de mon entrée? 2) À quel point mon compilateur est-il intelligent? 3) Comment mon runtime gère-t-il la mémoire? Ce n'est pas exhaustif, mais c'est un point de départ.

  1. Quelle est la taille de mon entrée?

    Une solution complexe aura souvent un temps système fixe, peut-être sous la forme d'opérations supplémentaires à effectuer, ou peut-être en mémoire supplémentaire nécessaire. Étant donné que ces solutions sont conçues pour gérer de gros cas, les implémenteurs n'auront généralement aucun problème à introduire ce coût supplémentaire, car le gain net est plus important que la micro-optimisation du code. Donc, si votre entrée est suffisamment petite, une solution naïve pourrait bien avoir de meilleures performances que la solution complexe, ne serait-ce que pour éviter ce surcoût. (déterminer ce qui est "suffisamment petit" est cependant la partie difficile)

  2. À quel point mon compilateur est-il intelligent?

    De nombreux compilateurs sont suffisamment intelligents pour "optimiser" les variables qui sont écrites, mais jamais lues. De même, un bon compilateur pourrait également convertir une concaténation de chaîne naïve en une utilisation (principale) de la bibliothèque et, si beaucoup d'entre elles sont effectuées sans aucune lecture, il n'est pas nécessaire de la reconvertir en chaîne entre ces opérations (même si votre code source semble faire exactement cela). Je ne peux pas dire si des compilateurs le font ou si cela est fait (AFAIK Java remplace au moins plusieurs concats dans la même expression par une séquence d'opérations StringBuffer), mais c'est une possibilité.

  3. Comment mon runtime gère-t-il la mémoire?

    Dans les processeurs modernes, le goulot d'étranglement n'est généralement pas le processeur, mais le cache; si votre code accède à de nombreuses adresses mémoire "distantes" en peu de temps, le temps nécessaire pour déplacer toute cette mémoire entre les niveaux de cache dépasse la plupart des optimisations dans les instructions utilisées. Cela est particulièrement important dans les exécutions avec des récupérateurs de génération, car les variables les plus récemment créées (à l'intérieur de la même étendue de fonction, par exemple) seront généralement dans des adresses mémoire contiguës. Ces runtimes déplacent également régulièrement la mémoire dans les deux sens entre les appels de méthode.

    Une façon dont cela peut affecter la concaténation de chaînes (avertissement: c'est une supposition sauvage, je ne suis pas suffisamment informé pour dire avec certitude) serait si la mémoire pour le naïf était allouée près du reste du code qui l'utilise (même s'il l'alloue et le libère plusieurs fois), alors que la mémoire de l'objet de bibliothèque a été allouée loin de lui (donc le nombre de changements de contexte pendant que votre code calcule, la bibliothèque consomme, votre code calcule plus, etc. générerait de nombreux ratés de cache). Bien sûr, pour les grandes entrées OTOH, les erreurs de cache se produiront de toute façon, de sorte que le problème des allocations multiples devient plus prononcé.

Cela dit, je ne préconise pas l'utilisation de telle ou telle méthode, mais seulement que les tests, le profilage et l'analyse comparative devraient précéder toute analyse théorique sur les performances, car la plupart des systèmes sont aujourd'hui trop complexes pour être pleinement compris sans une expertise approfondie dans le sujet.

mgibsonbr
la source
Oui, je suis d'accord que c'est certainement un domaine où un compilateur pourrait théoriquement réaliser que vous essayez d'ajouter un tas de chaînes ensemble et ensuite d'optimiser comme si vous utilisiez un générateur de chaînes. Cependant, ce n'est pas une chose triviale à faire, et je ne pense pas que cela soit implémenté dans les compilateurs modernes. Vous venez de me donner une excellente idée pour un projet de recherche de premier cycle: D.
JSideris
Vérifiez cette réponse , le compilateur Java utilise déjà StringBuildersous le capot, tout ce qu'il aurait à faire est de ne pas appeler toStringjusqu'à ce que la variable soit réellement nécessaire. Si je me souviens bien, il le fait pour une seule expression, mon seul doute est de savoir s'il s'applique ou non à plusieurs déclarations dans la même méthode. Je ne sais rien sur les internes .NET, mais je pense qu'une stratégie similaire pourrait également être utilisée par le compilateur C #.
mgibsonbr
0

Joel a écrit un excellent article sur ce sujet il y a quelque temps. Comme certains l'ont souligné, cela dépend fortement de la langue. En raison de la façon dont les chaînes sont implémentées en C (terminaison zéro, sans champ de longueur), la routine de bibliothèque strcat standard est très inefficace. Joel présente une alternative avec juste un changement mineur qui est beaucoup plus efficace.

tcrosley
la source
-1

Est-il inefficace de concaténer des chaînes une à la fois?

Non.

Avez-vous lu «La triste tragédie du théâtre de micro-optimisation» ?

Jim G.
la source
4
"L'optimisation prématurée est la racine de tout Mal." - Knuth
Scott C Wilson
4
La racine de tout mal dans l'optimisation est de prendre cette phrase sans contexte.
OZ_
Dire simplement que quelque chose est vrai sans fournir de raisons à l'appui n'est pas utile sur un forum comme celui-ci.
Edward Strange
@ Crazy Eddie: Avez-vous lu pourquoi Jeff Atwood avait à dire?
Jim G.