La démonstration de la récupération de place est plus rapide que la gestion manuelle de la mémoire

23

J'ai lu dans de nombreux endroits (diable, je l'ai même écrit moi-même) que la collecte des ordures pourrait (théoriquement) être plus rapide que la gestion manuelle de la mémoire.

Cependant, montrer est beaucoup plus difficile à trouver qu'à raconter.
Je n'ai jamais vu de morceau de code qui démontre cet effet en action.

Quelqu'un a-t-il (ou sait-il où je peux trouver) du code qui démontre cet avantage de performance?

Mehrdad
la source
5
le problème avec GC est que la plupart des implémentations ne sont pas déterministes, donc 2 exécutions peuvent avoir des résultats très différents, sans oublier qu'il est difficile d'isoler les bonnes variables à comparer
ratchet freak
@ratchetfreak: Si vous connaissez des exemples qui sont seulement plus rapides (disons) 70% du temps, ça me convient aussi. Il doit y avoir un moyen de comparer les deux, en termes de débit au moins (la latence ne fonctionnerait probablement pas).
Mehrdad
1
Eh bien, c'est un peu délicat car vous pouvez toujours faire manuellement tout ce qui donne au GC un avantage sur ce que vous avez fait manuellement. Peut-être est-il préférable de limiter cela aux outils de gestion manuelle de la mémoire "standard" (malloc () / free (), pointeurs possédés, pointeurs partagés avec refcount, pointeurs faibles, pas d'allocateurs personnalisés)? Ou, si vous autorisez des allocateurs personnalisés (qui peuvent être plus réalistes ou moins réalistes, selon le type de programmeur que vous supposez), imposez des restrictions sur l'effort investi dans ces allocateurs. Sinon, la stratégie manuelle "copier ce que fait le GC dans ce cas" est toujours au moins aussi rapide que le GC.
1
Par "copier ce que fait le GC", je ne voulais pas "construire votre propre GC" (mais notez que cela est théoriquement possible en C ++ 11 et au-delà, ce qui introduit la prise en charge facultative d'un GC). Je voulais dire, comme je l'ai dit plus tôt dans le même commentaire, "faire ce qui donne au GC un avantage sur ce que vous avez fait manuellement". Par exemple, si le compactage de type Cheney aide beaucoup cette application, vous pouvez implémenter manuellement un schéma d'allocation + de compactage similaire, avec des pointeurs intelligents personnalisés pour gérer la correction du pointeur. De plus, avec des techniques comme une pile d'ombres, vous pouvez effectuer une recherche de racine en C ou C ++, au détriment d'un travail supplémentaire.
1
@Ike: Ça va. Vous voyez pourquoi j'ai posé la question? C'était tout l'objet de ma question - les gens proposent toutes sortes d'explications qui devraient avoir du sens, mais tout le monde trébuche lorsque vous leur demandez de faire une démonstration qui prouve que ce qu'ils disent est correct dans la pratique. L'intérêt de cette question était de montrer une fois pour toutes que cela peut effectivement se produire dans la pratique.
Mehrdad

Réponses:

26

Voir http://blogs.msdn.com/b/ricom/archive/2005/05/10/416151.aspx et suivez tous les liens pour voir Rico Mariani vs Raymond Chen (deux programmeurs très compétents chez Microsoft) se battre . Raymond améliorerait le non managé, Rico réagirait en optimisant la même chose dans les managés.

Avec un effort d'optimisation pratiquement nul, les versions gérées ont démarré plusieurs fois plus rapidement que le manuel. Finalement, le manuel a battu le managé, mais seulement en optimisant à un niveau auquel la plupart des programmeurs ne voudraient pas aller. Dans toutes les versions, l'utilisation de la mémoire du manuel était nettement meilleure que celle gérée.

btilly
la source
+1 pour citer un exemple réel avec du code :) bien que l'utilisation correcte des constructions C ++ (comme swap) n'est pas si difficile, et vous y arriverait probablement assez facilement en termes de performances ...
Mehrdad
5
Vous pourrez peut-être surpasser Raymond Chen sur la performance. Je suis convaincu que je ne peux pas à moins qu'il ne s'en sorte à cause de sa maladie, je travaille beaucoup plus dur et j'ai eu de la chance. Je ne sais pas pourquoi il n'a pas choisi la solution que vous auriez choisie. Je suis sûr qu'il avait des raisons à cela
btilly
J'ai copié le code de Raymond ici , et pour comparer, j'ai écrit ma propre version ici . Le fichier ZIP qui contient le fichier texte est ici . Sur mon ordinateur, le mien fonctionne en 14 ms et celui de Raymond en 21 ms. Sauf si j'ai fait quelque chose de mal (ce qui est possible), son code de 215 lignes est 50% plus lent que mon implémentation de 48 lignes, même sans utiliser de fichiers mappés en mémoire ou de pools de mémoire personnalisés (qu'il a utilisés). Le mien est moitié moins long que la version C #. L'ai-je mal fait ou avez-vous observé la même chose?
Mehrdad
1
@Mehrdad En retirant une ancienne copie de gcc sur cet ordinateur portable, je peux signaler que ni votre code ni sa volonté ne se compileront, encore moins faire quoi que ce soit avec. Le fait que je ne sois pas sous Windows l'explique probablement. Mais supposons que vos numéros et votre code soient corrects. Font-ils la même chose sur un compilateur et un ordinateur vieux de dix ans? (Regardez quand le blog a été écrit.) Peut-être, peut-être pas. Supposons qu'ils le soient, qu'il (étant un programmeur C) ne savait pas comment utiliser correctement C ++, etc. Que nous reste-t-il?
btilly
1
Il nous reste un programme C ++ raisonnable qui peut être traduit en mémoire gérée et accéléré. Mais où la version C ++ peut être optimisée et accélérée plus loin. Ce que nous sommes tous d'accord, c'est le schéma général qui se produit toujours lorsque le code managé est plus rapide que non managé. Cependant, nous avons toujours un exemple concret de code raisonnable d'un bon programmeur qui était plus rapide dans une version gérée.
btilly
5

La règle d'or est qu'il n'y a pas de repas gratuits.

GC élimine le casse-tête de la gestion manuelle de la mémoire et réduit la probabilité de faire des erreurs. Dans certaines situations, une stratégie GC particulière est la solution optimale au problème, auquel cas vous ne paierez aucune pénalité pour son utilisation. Mais il y en a d'autres où d'autres solutions seront plus rapides. Comme vous pouvez toujours simuler des abstractions plus élevées à partir d'un niveau inférieur, mais pas l'inverse, vous pouvez effectivement prouver qu'il n'y a aucun moyen que des abstractions plus élevées puissent être plus rapides que les plus basses dans le cas général.

GC est un cas particulier de gestion manuelle de la mémoire

Obtenir de meilleures performances manuellement peut demander beaucoup de travail ou davantage d'erreurs, mais c'est une autre histoire.

Guy Sirton
la source
1
Cela n'a aucun sens pour moi. Pour vous donner quelques exemples concrets: 1) les allocateurs et les barrières d'écriture dans les GC de production sont des assembleurs écrits à la main parce que C est trop inefficace alors comment allez-vous battre cela à partir de C, et 2) l'élimination des appels de queue est un exemple d'optimisation fait dans des langages de haut niveau (fonctionnels) qui n'est pas fait par le compilateur C et, par conséquent, ne peut pas être fait dans C. La marche de pile est un autre exemple de quelque chose fait en dessous du niveau de C par des langages de haut niveau.
Jon Harrop
2
1) Je devrais voir le code spécifique pour commenter mais si les allocateurs / barrières écrits à la main dans l'assembleur sont plus rapides, utilisez l'assembleur écrit à la main. Je ne sais pas ce que cela a à voir avec GC. 2) Jetez un oeil ici: stackoverflow.com/a/9814654/441099 le point n'est pas de savoir si un langage non GC peut faire l'élimination de la récursivité de queue pour vous. Le fait est que vous pouvez transformer votre code pour qu'il soit aussi rapide ou plus rapide. Que le compilateur d'un langage spécifique puisse le faire automatiquement pour vous est une question de commodité. Dans une abstraction suffisamment basse, vous pouvez toujours le faire vous-même si vous le souhaitez.
Guy Sirton
1
Cet exemple d'appel de queue en C ne fonctionne que pour le cas particulier d'une fonction qui s'appelle elle-même. C ne peut pas gérer le cas général des fonctions qui s'appellent les unes les autres. Passer à l'assembleur et supposer un temps infini pour le développement est un tarpit de Turing.
Jon Harrop
3

Il est facile de construire une situation artificielle où GC est infiniment plus efficace que les méthodes manuelles - arrangez-vous simplement pour qu'il n'y ait qu'une seule "racine" pour le garbage collector, et que tout soit ordures, donc l'étape GC est immédiatement terminée.

Si vous y réfléchissez, c'est le modèle utilisé lors de la collecte de la mémoire allouée à un processus. Le processus meurt, tout sa mémoire est des ordures, nous avons terminé. Même en termes pratiques, un processus qui démarre, s'exécute et s'éteint sans laisser de trace peut être plus efficace qu'un processus qui démarre et s'exécute pour toujours.

Pour les programmes pratiques, écrits dans des langues avec garbage collection, l'avantage du garbage collection n'est pas la rapidité mais la justesse et la simplicité.

ddyer
la source
S'il est facile de construire un exemple artificiel, voudriez-vous en montrer un simple?
Mehrdad
1
@Mehrdad Il en a expliqué une simple. Écrivez un programme dans lequel la version GC ne parvient pas à exécuter une exécution incorrecte avant de quitter. La version gérée en mémoire manuelle sera plus lente car elle traçait et libérait explicitement des éléments.
btilly
3
@btilly: "Ecrivez un programme dans lequel la version du GC ne parvient pas à exécuter un nettoyage avant de quitter." ... le fait de ne pas faire de ramasse-miettes en premier lieu est une fuite de mémoire due à l' absence d'un GC fonctionnel, pas une amélioration des performances due à la présence d'un GC! C'est comme appeler abort()en C ++ avant la fin du programme. C'est une comparaison dénuée de sens; vous n'êtes même pas en train de ramasser les ordures, vous laissez simplement la mémoire fuir. Vous ne pouvez pas dire que la collecte des ordures est plus rapide (ou plus lente) si vous ne commencez pas par la collecte des ordures ...
Mehrdad
Pour faire un exemple extrême, vous devez définir un système complet avec votre propre tas et gestion de tas, ce qui serait un excellent projet étudiant mais trop grand pour tenir dans cette marge. Vous feriez plutôt bien en écrivant un programme qui alloue et désalloue des tableaux de taille aléatoire, d'une manière conçue pour être stressante pour les méthodes de gestion de la mémoire non gc.
ddyer
3
@Mehrdad Pas vrai. Le scénario est que la version GC n'a jamais atteint le seuil auquel elle aurait effectué une exécution, pas qu'elle n'aurait pas réussi à fonctionner correctement sur un autre ensemble de données. Cela va être très bon pour la version GC, bien que ce ne soit pas un bon prédicteur des performances éventuelles.
btilly
2

Il faut considérer que GC n'est pas seulement une stratégie de gestion de la mémoire; il impose également des exigences sur la conception complète de l'environnement de langage et d'exécution, ce qui impose des coûts (et des avantages). Par exemple, un langage qui prend en charge GC doit être compilé sous une forme où les pointeurs ne peuvent pas être cachés au garbage collector, et généralement où ils ne peuvent être construits que par des primitives système soigneusement gérées. Une autre considération est la difficulté de maintenir les garanties de temps de réponse, car le GC impose certaines étapes qui doivent pouvoir se terminer.

Par conséquent, si vous avez une langue qui est garbage collection et comparez la vitesse avec la mémoire gérée manuellement dans le même système, vous devez toujours payer les frais généraux pour prendre en charge le garbage collection même si vous ne l'utilisez pas.

ddyer
la source
2

Plus vite est douteux. Cependant, il peut être ultra-rapide, imperceptible ou plus rapide s'il est pris en charge par le matériel. Il y a longtemps, il y avait des modèles comme celui-là pour les machines LISP. L'un a intégré le GC dans le sous-système de mémoire du matériel en tant que tel que le processeur principal ne savait pas qu'il était là. Comme beaucoup de conceptions ultérieures, le GC a fonctionné simultanément avec le processeur principal avec peu ou pas besoin de pauses. Une conception plus moderne est celle des machines Azul Systems Vega 3 qui exécutent le code Java beaucoup plus rapidement que les machines virtuelles Java utilisant des processeurs spécialement conçus et un GC sans pause. Utilisez-les sur Google si vous voulez savoir à quelle vitesse GC (ou Java) peut être.

Nick P
la source
2

J'ai fait pas mal de travail à ce sujet et en ai décrit une partie ici . J'ai comparé le GC Boehm en C ++, en allouant en utilisant mallocmais pas en libérant, en allouant et en libérant en utilisant freeun GC de région de marque personnalisé écrit en C ++ tout contre le GC stock OCaml exécutant un solveur n-queens basé sur une liste. Le GC d'OCaml était plus rapide dans tous les cas. Les programmes C ++ et OCaml ont été délibérément écrits pour effectuer les mêmes allocations dans le même ordre.

Vous pouvez bien sûr réécrire les programmes pour résoudre le problème en utilisant uniquement des entiers 64 bits et aucune allocation. Bien que plus rapide, cela irait à l'encontre de l'objectif de l'exercice (qui était de prédire les performances d'un nouvel algorithme GC sur lequel je travaillais en utilisant un prototype construit en C ++).

J'ai passé de nombreuses années dans l'industrie à porter du vrai code C ++ vers des langages gérés. Dans presque tous les cas, j'ai observé des améliorations de performances substantielles, dont beaucoup étaient probablement dues à la gestion manuelle de la mémoire par GC. La limite pratique n'est pas ce qui peut être accompli dans une microbenchmark mais ce qui peut être accompli avant une date limite et les langages basés sur GC offrent des améliorations de productivité si énormes que je n'ai jamais regardé en arrière. J'utilise toujours C et C ++ sur des appareils embarqués (microcontrôleurs) mais même cela change maintenant.

Jon Harrop
la source
+1 merci. Où pouvons-nous voir et exécuter le code de référence?
Mehrdad
Le code est dispersé sur le lieu. J'ai posté la version mark-region ici: groups.google.com/d/msg/…
Jon Harrop
1
Il y a des résultats pour les threads sûrs et non sécurisés là-dedans.
Jon Harrop
1
@Mehrdad: "Avez-vous éliminé ces sources potentielles d'erreur?". Oui. OCaml a un modèle de compilation très simple sans optimisations telles que l'analyse d'échappement. La représentation de la fermeture par OCaml est en réalité beaucoup plus lente que la solution C ++, donc elle devrait vraiment utiliser une personnalisation List.filtercomme le fait C ++. Mais, oui, vous avez certainement tout à fait raison de dire que certaines opérations RC peuvent être éludées. Cependant, le plus gros problème que je vois dans la nature est que les gens n'ont pas le temps d'effectuer de telles optimisations à la main sur de grandes bases de codes industriels.
Jon Harrop
2
Oui absolument. Aucun effort supplémentaire pour écrire mais écrire du code n'est pas le goulot d'étranglement avec C ++. Le maintien du code est. Le maintien du code avec ce type de complexité accessoire est un cauchemar. La plupart des bases de code industrielles sont des millions de lignes de code. Vous ne voulez tout simplement pas avoir à gérer cela. J'ai vu des gens tout convertir shared_ptrpour corriger des bugs de concurrence. Le code est beaucoup plus lent mais bon, maintenant ça marche.
Jon Harrop
-1

Un tel exemple a nécessairement un mauvais schéma d'allocation de mémoire manuelle.

Supposons le meilleur ramasse-miettes GC. Il a en interne des méthodes pour allouer de la mémoire, déterminer quelle mémoire peut être libérée et des méthodes pour enfin la libérer. Ensemble, cela prend moins de temps que tous GC; un certain temps est consacré aux autres méthodes du GC.

Considérons maintenant un allocateur manuel qui utilise le même mécanisme d'allocation que GC, et dont l' free()appel met simplement de côté la mémoire à libérer par la même méthode que GC. Il n'a pas de phase de numérisation, ni aucune autre méthode. Cela prend nécessairement moins de temps.

MSalters
la source
2
Un garbage collector peut souvent libérer de nombreux objets, sans avoir à mettre la mémoire dans un état utile après chacun. Considérez la tâche de supprimer d'une liste de tableaux tous les éléments répondant à un certain critère. La suppression d'un seul élément d'une liste de N éléments est O (N); supprimer M éléments d'une liste de N, un à la fois est O (M * N). Supprimer tous les éléments répondant à un critère en un seul passage dans la liste, cependant, est O (1).
supercat
@supercat: freepeut également collecter des lots. (Et bien sûr, supprimer tous les éléments répondant à un critère est toujours O (N), ne serait-ce qu'en raison de la liste elle-même)
MSalters
La suppression de tous les éléments répondant à un critère est au moins O (N). Vous avez raison, cela freepourrait fonctionner dans un mode de collecte par lots si chaque élément de mémoire était associé à un indicateur, bien que le GC puisse toujours sortir en tête dans certaines situations. Si l'on a M références qui identifient L éléments distincts sur un ensemble de N choses, le temps de supprimer chaque référence à laquelle aucune référence n'existe et de consolider le reste est O (M) plutôt que O (N). Si l'on dispose de M d'espace supplémentaire disponible, la constante de mise à l'échelle peut être assez petite. De plus, la compactification dans un système GC sans balayage nécessite ...
supercat
@supercat: Eh bien, ce n'est certainement pas O (1) comme l'indique votre dernière phrase dans le premier commentaire.
MSalters
1
@MSalters: "Et qu'est-ce qui empêcherait un régime déterministe d'avoir une pépinière?". Rien. Le ramasse-miettes d'OCaml est déterministe et utilise une pépinière. Mais ce n'est pas "manuel" et je pense que vous abusez du mot "déterministe".
Jon Harrop