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?
la source
Réponses:
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
StringBuilder
etStringBuffer
existeraient-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:
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.
la source
strA + strB
est 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/…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.
la source
À 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."
la source
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.
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)
À 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é.
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.
la source
StringBuilder
sous le capot, tout ce qu'il aurait à faire est de ne pas appelertoString
jusqu'à 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 #.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.
la source
Non.
Avez-vous lu «La triste tragédie du théâtre de micro-optimisation» ?
la source