web-dev-qa-db-fra.com

Qu'est-ce que la fragmentation de la mémoire?

J'ai entendu le terme "fragmentation de la mémoire" utilisé à quelques reprises dans le contexte de l'allocation de mémoire dynamique C++. J'ai trouvé quelques questions sur la manière de traiter la fragmentation de la mémoire, mais je ne peux pas trouver une question directe qui traite elle-même. Alors:

  • Qu'est-ce que la fragmentation de la mémoire?
  • Comment savoir si la fragmentation de la mémoire pose un problème pour mon application? Quel type de programme est le plus susceptible de souffrir?
  • Quels sont les bons moyens de gérer la fragmentation de la mémoire?

Aussi:

  • J'ai souvent entendu dire que l'utilisation d'allocations dynamiques pouvait augmenter la fragmentation de la mémoire. Est-ce vrai? Dans le contexte de C++, je comprends que tous les conteneurs standard (std :: string, std :: vector, etc.) utilisent une allocation de mémoire dynamique. Si ceux-ci sont utilisés dans tout un programme (en particulier std :: string), la fragmentation de la mémoire est-elle plus susceptible de poser problème?
  • Comment traiter la fragmentation de la mémoire dans une application lourde en LIST?
186
AshleysBrain

Imaginez que vous ayez une "grande" étendue de mémoire libre (32 octets):

----------------------------------
|                                |
----------------------------------

Maintenant, allouez une partie de celle-ci (5 allocations):

----------------------------------
|aaaabbccccccddeeee              |
----------------------------------

Maintenant, libérez les quatre premières attributions mais pas la cinquième:

----------------------------------
|              eeee              |
----------------------------------

Maintenant, essayez d'allouer 16 octets. Oups, je ne peux pas, même s'il y a presque le double de ce qui est libre.

Sur les systèmes dotés de mémoire virtuelle, la fragmentation pose moins de problèmes que vous ne le pensez, car les allocations importantes doivent uniquement être contiguës dans l'espace d'adressage virtuel, pas dans l'espace d'adressage physique. Ainsi, dans mon exemple, si j’avais de la mémoire virtuelle avec une taille de page de 2 octets, je pourrais effectuer mon allocation de 16 octets sans problème. La mémoire physique ressemblerait à ceci:

----------------------------------
|ffffffffffffffeeeeff            |
----------------------------------

alors que la mémoire virtuelle (étant beaucoup plus grande) pourrait ressembler à ceci:

------------------------------------------------------...
|              eeeeffffffffffffffff                   
------------------------------------------------------...

Le symptôme classique de la fragmentation de la mémoire est que vous essayez d'allouer un gros bloc et que vous ne pouvez pas, même si vous semblez avoir suffisamment de mémoire. Une autre conséquence possible est l’incapacité du processus à restituer de la mémoire au système d’exploitation (car un objet est toujours utilisé dans tous les blocs qu’il a alloué à partir du système d’exploitation, même si ces blocs sont pour la plupart inutilisés).

Les tactiques pour empêcher la fragmentation de la mémoire en C++ fonctionnent en allouant des objets de zones différentes en fonction de leur taille et/ou de leur durée de vie prévue. Donc, si vous allez créer beaucoup d'objets et les détruire tous ensemble plus tard, allouez-les à partir d'un pool de mémoire. Toutes les autres affectations que vous faites entre eux ne feront pas partie du pool et ne seront donc pas situés entre eux en mémoire. Par conséquent, la mémoire ne sera pas fragmentée.

En règle générale, vous n'avez pas besoin de trop vous inquiéter, à moins que votre programme dure depuis longtemps et fasse beaucoup d'allocation et de libération. C'est lorsque vous avez des mélanges d'objets de courte et de longue durée que vous êtes le plus à risque, mais même dans ce cas, malloc fera de son mieux pour vous aider. En règle générale, ignorez-le jusqu'à ce que votre programme échoue ou que le système manque de mémoire de manière inattendue (prenez le temps de le tester, de préférence!).

Les bibliothèques standard ne sont pas pires que celles qui allouent de la mémoire. Les conteneurs standard ont tous un paramètre de modèle Alloc que vous pouvez utiliser pour affiner leur stratégie d’allocation si cela est absolument nécessaire.

285
Steve Jessop

Qu'est-ce que la fragmentation de la mémoire?

La fragmentation de la mémoire se produit lorsque la majeure partie de votre mémoire est allouée dans un grand nombre de blocs non contigus, ce qui laisse un bon pourcentage de votre mémoire totale non allouée, mais inutilisable pour la plupart des scénarios classiques. Cela entraîne des exceptions de mémoire insuffisante ou des erreurs d’allocation (c’est-à-dire que malloc renvoie null).

La meilleure façon de penser à cela est d’imaginer que vous avez un grand mur vide sur lequel vous devez placer des images de différentes tailles . Chaque image prend une certaine taille et vous ne pouvez évidemment pas la diviser en petits morceaux pour l'adapter. Vous avez besoin d'un emplacement vide sur le mur, de la taille de l'image, sinon vous ne pouvez pas l'afficher. Maintenant, si vous commencez à accrocher des images au mur et que vous ne faites pas attention à la manière dont vous les arrangez, vous vous retrouverez bientôt avec un mur partiellement recouvert d'images. Même si vous avez des espaces vides, la plupart des nouvelles images ne vous iront pas. parce qu'ils sont plus grands que les places disponibles. Vous pouvez toujours accrocher de très petites images, mais la plupart d’entre elles ne conviendront pas. Il vous faudra donc réorganiser (compacter) celles déjà accrochées au mur pour faire de la place pour plus.

Maintenant, imaginez que le mur est votre mémoire (tas) et que les images sont des objets .. C’est une fragmentation de la mémoire ..

Comment puis-je savoir si la fragmentation de la mémoire est un problème pour mon application? Quel type de programme est le plus susceptible de souffrir?

Le fait que vous rencontriez de nombreuses erreurs d'allocation, en particulier lorsque le pourcentage de mémoire utilisé est élevé, constitue un signe révélateur de la fragmentation de la mémoire, mais que vous n'avez pas encore épuisé toute sa mémoire, vous devez donc disposer de suffisamment d'espace. pour les objets que vous essayez d'allouer.

Lorsque la mémoire est fortement fragmentée, les allocations de mémoire prendront probablement plus de temps car l’allocateur de mémoire doit s’efforcer davantage de trouver un espace approprié pour le nouvel objet. Si, à son tour, vous avez beaucoup d’allocations de mémoire (ce que vous avez probablement fait depuis que vous avez fini avec la fragmentation de la mémoire), le temps d’allocation peut même entraîner des retards notables.

Quels sont les bons moyens de gérer la fragmentation de la mémoire?

Utilisez un bon algorithme pour allouer de la mémoire. Au lieu d'allouer de la mémoire pour un grand nombre de petits objets, pré-allouez de la mémoire pour un tableau contigu de ces plus petits objets. Parfois, un peu de gaspillage lors de l’allocation de mémoire peut avoir un impact positif sur les performances et peut vous éviter des problèmes de fragmentation de la mémoire.

70
Mike Dinescu

La fragmentation de la mémoire est le même concept que la fragmentation du disque: elle fait référence à un espace gaspillé, car les zones utilisées ne sont pas assez proches les unes des autres.

Supposons, pour un exemple de jouet simple, que vous avez dix octets de mémoire:

 |   |   |   |   |   |   |   |   |   |   |
   0   1   2   3   4   5   6   7   8   9

Attribuons maintenant trois blocs de trois octets, nommés A, B et C:

 | A | A | A | B | B | B | C | C | C |   |
   0   1   2   3   4   5   6   7   8   9

Désallouez maintenant le bloc B:

 | A | A | A |   |   |   | C | C | C |   |
   0   1   2   3   4   5   6   7   8   9

Que se passe-t-il maintenant si nous essayons d'allouer un bloc D de quatre octets? Eh bien, nous avons quatre octets de mémoire libre, mais nous n'avons pas quatre octets contigus de mémoire libre, nous ne pouvons donc pas allouer D! Ceci est une utilisation inefficace de la mémoire, car nous aurions dû pouvoir stocker D, mais nous n’avons pas pu. Et nous ne pouvons pas déplacer C pour faire de la place, car il est très probable que certaines variables de notre programme pointent vers C, et nous ne pouvons pas automatiquement rechercher et modifier toutes ces valeurs.

Comment savez-vous que c'est un problème? Eh bien, le plus gros signe est que la taille de la mémoire virtuelle de votre programme est considérablement plus grande que la quantité de mémoire que vous utilisez réellement. Dans un exemple réel, vous disposerez de beaucoup plus de dix octets de mémoire. Par conséquent, D serait simplement alloué en commençant par l'octet 9 et les octets 3 à 5 resteraient inutilisés, à moins que vous n'allouiez par la suite un élément de trois octets ou moins.

Dans cet exemple, 3 octets ne sont pas énormément à perdre, mais considérons un cas plus pathologique où deux allocations de quelques octets sont, par exemple, dix mégaoctets en mémoire, et vous devez allouer un bloc de taille 10 mégaoctets. + 1 octet. Vous devez demander au système d’exploitation plus de 10 Mo de mémoire virtuelle supplémentaire pour le faire, même si vous n’êtes qu’à un octet d’avoir déjà assez d’espace.

Comment l'évitez-vous? Les pires cas ont tendance à se produire lorsque vous créez et détruisez fréquemment de petits objets, car cela a tendance à produire un effet de "fromage suisse" avec de nombreux petits objets séparés par de nombreux petits trous, rendant impossible l'attribution d'objets plus grands dans ces trous. Une stratégie efficace consiste à pré-allouer un grand bloc de mémoire en tant que pool pour vos petits objets, puis à gérer manuellement la création de petits objets dans ce bloc, plutôt que de laisser l'allocateur par défaut le gère.

En général, moins vous effectuez d'allocation, moins la mémoire risque d'être fragmentée. Cependant, STL gère cela assez efficacement. Si vous avez une chaîne qui utilise l'intégralité de son allocation actuelle et que vous y ajoutez un caractère, elle ne ré-attribue pas simplement à sa longueur actuelle plus un, elle double sa longueur. Il s’agit d’une variante de la stratégie "pool pour les petites allocations fréquentes". La chaîne récupère une grande quantité de mémoire, ce qui lui permet de gérer efficacement les petites augmentations répétées sans faire de petites réaffectations répétées. Tous les conteneurs STL agissent en fait de la sorte. Vous n'avez donc généralement pas à vous inquiéter de la fragmentation provoquée par la réallocation automatique des conteneurs STL.

Bien sûr, les conteneurs STL ne regroupent pas la mémoire entre , donc si vous allez créer de nombreux petits conteneurs (plutôt que quelques conteneurs redimensionné fréquemment), vous devrez peut-être vous empêcher de fragmenter de la même manière que vous le feriez pour de petits objets fréquemment créés, en LIST ou non.

23
Tyler McHenry
  • Qu'est-ce que la fragmentation de la mémoire?

La fragmentation de la mémoire est le problème de la mémoire qui devient inutilisable, même si elle est théoriquement disponible. Il existe deux types de fragmentation: fragmentation interne est la mémoire qui est allouée mais ne peut pas être utilisée (par exemple, lorsque la mémoire est allouée en blocs de 8 octets mais que le programme effectue à plusieurs reprises des allications uniques lorsqu'il n'a besoin que de 4 octets). fragmentation externe est le problème de la mémoire libre en train de se diviser en plusieurs petits morceaux de sorte que les demandes d'allocation importantes ne peuvent pas être satisfaites bien qu'il y ait assez de mémoire libre globale.

  • Comment savoir si la fragmentation de la mémoire pose un problème pour mon application? Quel type de programme est le plus susceptible de souffrir?

la fragmentation de la mémoire pose un problème si votre programme utilise beaucoup plus de mémoire système que ne le nécessitent les données de paylod réelles (et vous avez exclu les fuites de mémoire).

  • Quels sont les bons moyens de gérer la fragmentation de la mémoire?

Utilisez un bon allocateur de mémoire. IIRC, ceux qui utilisent une stratégie de "meilleur ajustement" sont généralement bien supérieurs pour éviter la fragmentation, même s’ils sont un peu plus lents. Cependant, il a également été démontré que, quelle que soit la stratégie d'allocation, il existe des cas pathologiques extrêmes. Heureusement, les schémas d'allocation typiques de la plupart des applications sont en réalité relativement inoffensifs pour les allocateurs. Si vous êtes intéressé par les détails, il existe de nombreux articles:

  • Paul R. Wilson, Mark S. Johnstone, Michael Neely et David Boles. Allocation de stockage dynamique: enquête et examen critique. Dans Actes de l'atelier international de 1995 sur la gestion de la mémoire, Springer Verlag LNCS, 1995
  • Mark S.Johnstone, Paul R. Wilson. Le problème de fragmentation de la mémoire: résolu? Dans les notices ACM SIG-PLAN, volume 34 n ° 3, pages 26-36, 1999
  • M.R. Garey, R.L. Graham et J.D. Ullman. Analyse des pires cas des algorithmes d'allocation de mémoire. Dans le quatrième symposium annuel ACM sur la théorie de l'informatique, 1972
14
Michael Borgwardt

Mise à jour:
Google TCMalloc: Malloc avec mise en cache de threads
Il a été constaté que est assez efficace pour gérer la fragmentation dans un processus de longue durée.


Je développais une application serveur qui rencontrait des problèmes de fragmentation de la mémoire sur HP-UX 11.23/11.31 ia64.

Cela ressemblait à ceci. Il y avait un processus qui faisait des allocations de mémoire et des désallocations et fonctionnait pendant des jours. Et même s’il n’y avait pas de fuites de mémoire, la consommation de mémoire du processus n’a cessé d’augmenter.

A propos de mon expérience. Sur HP-UX, il est très facile de trouver une fragmentation de la mémoire à l'aide de HP-UX gdb. Vous définissez un point d'arrêt et lorsque vous le frappez, vous exécutez cette commande: info heap et voir toutes les allocations de mémoire pour le processus et la taille totale du segment de mémoire. Ensuite, vous continuez votre programme et, quelque temps plus tard, vous atteignez de nouveau le point de rupture. Tu fais encore info heap. Si la taille totale du segment de mémoire est plus grande mais que le nombre et la taille des allocations séparées sont identiques, il est probable que vous rencontriez des problèmes d'allocation de mémoire. Si nécessaire, vérifiez quelques temps avant.

Ma façon d'améliorer la situation était la suivante. Après une analyse avec HP-UX gdb, j'ai constaté que les problèmes de mémoire étaient dus au fait que j'ai utilisé std::vector pour stocker certains types d'informations d'une base de données. std::vector nécessite que ses données soient conservées dans un bloc. J'ai eu quelques conteneurs basés sur std::vector. Ces conteneurs ont été régulièrement recréés. Il arrivait souvent que de nouveaux enregistrements soient ajoutés à la base de données, puis que les conteneurs soient recréés. Et comme les conteneurs recréés étaient plus grands, ils ne correspondaient pas aux blocs de mémoire libre disponibles et le moteur d’exécution demandait un nouveau bloc plus grand au système d’exploitation. En conséquence, bien qu’il n’y ait pas eu de fuite de mémoire, la consommation de mémoire du processus a augmenté. J'ai amélioré la situation lorsque j'ai changé les conteneurs. Au lieu de std::vector J'ai commencé à utiliser std::deque qui utilise une méthode différente pour allouer de la mémoire aux données.

Je sais que l’un des moyens d’éviter la fragmentation de la mémoire sur HP-UX consiste à utiliser soit l’allocateur de petits blocs, soit MallocNextGen. Sous RedHat Linux, l’allocateur par défaut semble bien gérer l’allocation de nombreux petits blocs. Sous Windows, il y a Low-fragmentation Heap et cela règle le problème du grand nombre de petites allocations.

D'après ce que je comprends, dans une application lourde STL, vous devez d'abord identifier les problèmes. Les allocateurs de mémoire (comme dans libc) traitent en réalité le problème de nombreuses petites affectations, ce qui est typique pour std::string _ (par exemple, dans mon application serveur, il y a beaucoup de chaînes STL mais, comme je le constate en exécutant info heap ils ne causent aucun problème). Mon impression est que vous devez éviter les allocations fréquentes et importantes. Malheureusement, il existe des situations dans lesquelles vous ne pouvez pas les éviter et devez changer votre code. Comme je le dis dans mon cas, j’ai amélioré la situation en passant à std::deque. Si vous identifiez votre fragment de mémoire, vous pourrez peut-être en parler plus précisément.

9
Sergei Kurenkov

Une réponse très détaillée sur la fragmentation de la mémoire peut être trouvée ici.

http://library.softwareverify.com/memory-fragmentation-your-worst-nightmare/

C’est le point culminant de onze années de réponses à la fragmentation de la mémoire que j’ai fournies aux personnes qui me posaient des questions sur la fragmentation de la mémoire sur softwareverify.com.

6
Stephen Kellett

La fragmentation de la mémoire est plus susceptible de se produire lorsque vous allouez et libérez de nombreux objets de tailles variables. Supposons que vous ayez la disposition suivante en mémoire:

obj1 (10kb) | obj2(20kb) | obj3(5kb) | unused space (100kb)

Désormais, lorsque obj2 Est publié, vous disposez de 120 Ko de mémoire non utilisée, mais vous ne pouvez pas allouer un bloc complet de 120 Ko, car la mémoire est fragmentée.

Les techniques courantes pour éviter cet effet sont les suivantes: tampons en annea et pools d'objets . Dans le contexte de la STL, des méthodes telles que std::vector::reserve() peuvent vous aider.

6
Björn Pollex

Ceci est une version super simplifiée pour les nuls.

Lorsque les objets sont créés en mémoire, ils sont ajoutés à la fin de la partie utilisée en mémoire.

Si un objet qui n'est pas à la fin de la partie utilisée de la mémoire est supprimé, ce qui signifie que cet objet se trouvait entre 2 autres objets, un "trou" sera créé.

C'est ce qu'on appelle la fragmentation.

3
user455288

Qu'est-ce que la fragmentation de la mémoire?

Lorsque votre application utilise la mémoire dynamique, elle alloue et libère des morceaux de mémoire. Au début, l’espace mémoire total de votre application correspond à un bloc contigu de mémoire libre. Cependant, lorsque vous allouez et libérez des blocs de taille différente, la mémoire commence à se fragmenter , c’est-à-dire à la place d’un gros bloc libre contigu et d’un nombre de contigu blocs alloués, il y aura mélange de blocs alloués et libres. Les blocs libres ayant une taille limitée, il est difficile de les réutiliser. Par exemple. vous pouvez avoir 1 000 octets de mémoire libre, mais vous ne pouvez pas allouer de mémoire pour un bloc de 100 octets, car tous les blocs libres ont une longueur maximale de 50 octets.

Une autre source de fragmentation inévitable mais moins problématique est que, dans la plupart des architectures, les adresses mémoire doivent être alignées sur des limites d'octet de 2, 4, 8 etc. ( les adresses doivent être des multiples de 2, 4, 8, etc.). Cela signifie que même si vous avez par exemple une structure contenant 3 char champs, votre structure peut avoir une taille de 12 au lieu de 3, car chaque champ est aligné sur une limite de 4 octets.

Comment savoir si la fragmentation de la mémoire pose un problème pour mon application? Quel type de programme est le plus susceptible de souffrir?

La réponse évidente est que vous obtenez une exception de mémoire insuffisante.

Apparemment, il n'y a pas de bon moyen portable de détecter la fragmentation de la mémoire dans les applications C++. Voir cette réponse pour plus de détails.

Quels sont les bons moyens de gérer la fragmentation de la mémoire?

C'est difficile en C++, car vous utilisez des adresses de mémoire directes dans les pointeurs et vous n'avez aucun contrôle sur qui fait référence à une adresse de mémoire spécifique. Donc, réorganiser les blocs de mémoire alloués (comme le fait le Java garbage collector)] n’est pas une option.

Un allocateur personnalisé peut aider en gérant l'allocation de petits objets dans une plus grande partie de la mémoire et en réutilisant les logements libres dans cette partie.

3
Péter Török

Lorsque vous souhaitez ajouter un élément sur le tas, l'ordinateur doit alors rechercher de l'espace pour l'adapter à cet élément. C'est pourquoi les allocations dynamiques, lorsqu'elles ne sont pas effectuées sur un pool de mémoire ou avec un allocateur en pool, peuvent "ralentir" les choses. Pour une application STL lourde, si vous utilisez le multi-threading, il existe la version Hoard allocator ou TBB Intel .

Maintenant, quand la mémoire est fragmentée, deux choses peuvent se produire:

  1. Il faudra plus de recherches pour trouver un bon espace pour coller des "gros" objets. C’est-à-dire qu’avec de nombreux petits objets éparpillés sur la recherche d’une belle mémoire, il pourrait être difficile dans certaines conditions (celles-ci sont extrêmes.)
  2. La mémoire n'est pas une entité facile à lire. Les processeurs sont limités à combien ils peuvent détenir et où. Ils le font en échangeant des pages si un élément dont ils ont besoin est un endroit, mais les adresses actuelles en sont un autre. Si vous devez constamment permuter des pages, le traitement peut être ralenti (là encore, dans des scénarios extrêmes où cela influe sur les performances.) Voir cette publication sur mémoire virtuelle .
2
wheaties

La fragmentation de la mémoire se produit parce que des blocs de mémoire de différentes tailles sont demandés. Considérons un tampon de 100 octets. Vous demandez deux caractères, puis un entier. Maintenant, vous libérez les deux caractères, puis demandez un nouvel entier, mais cet entier ne peut pas tenir dans l'espace des deux caractères. Cette mémoire ne peut pas être réutilisée car elle ne se trouve pas dans un bloc contigu suffisamment grand pour être réaffectée. En plus de cela, vous avez invoqué beaucoup de surcharge d'allocateur pour vos caractères.

Pour l’essentiel, la mémoire n’est disponible que par blocs d’une certaine taille sur la plupart des systèmes. Une fois que vous avez divisé ces blocs, ils ne peuvent plus être rejoints tant que le bloc entier n'a pas été libéré. Cela peut entraîner l'utilisation de blocs entiers alors qu'en réalité, seule une petite partie du bloc est utilisée.

Le principal moyen de réduire la fragmentation du tas consiste à effectuer des allocations plus grandes et moins fréquentes. Dans les cas extrêmes, vous pouvez utiliser un segment de mémoire géré capable de déplacer des objets, au moins, dans votre propre code. Cela élimine complètement le problème - du point de vue de la mémoire, de toute façon. Évidemment, les objets en mouvement ont un coût. En réalité, vous ne rencontrez vraiment de problème que si vous allouez souvent de très petites sommes. Le meilleur moyen de la réduire consiste à utiliser des conteneurs contigus (vecteur, chaîne, etc.) et à allouer autant que possible sur la pile (toujours une bonne idée de performance). Cela augmente également la cohérence du cache, ce qui accélère l'exécution de votre application.

Ce qu'il ne faut pas oublier, c'est que sur un système de bureau x86 32 bits, vous disposez de 2 Go de mémoire, divisée en "pages" de 4 Ko (vous êtes presque certain que la taille de la page est la même sur tous les systèmes x86). Vous aurez besoin d'appeler une fragmentation omgwtfbbq pour avoir un problème. La fragmentation est vraiment une question du passé, car les tas modernes sont excessivement grands pour la grande majorité des applications, et il existe une prédominance de systèmes capables de la supporter, tels que les tas gérés.

1
Puppy