Libérer de la mémoire en Python

128

J'ai quelques questions liées à l'utilisation de la mémoire dans l'exemple suivant.

  1. Si je cours dans l'interprète,

    foo = ['bar' for _ in xrange(10000000)]

    la vraie mémoire utilisée sur ma machine va jusqu'à 80.9mb. Moi alors,

    del foo

    la vraie mémoire descend, mais seulement pour 30.4mb. L'interpréteur utilise la 4.4mbligne de base, alors quel est l'avantage de ne pas libérer 26mbde mémoire vers le système d'exploitation? Est-ce parce que Python "planifie à l'avance", pensant que vous pourriez utiliser à nouveau autant de mémoire?

  2. Pourquoi libère-t-il 50.5mben particulier - sur quoi se base le montant libéré?

  3. Existe-t-il un moyen de forcer Python à libérer toute la mémoire utilisée (si vous savez que vous n'utiliserez plus autant de mémoire)?

REMARQUE Cette question est différente de Comment puis-je libérer explicitement de la mémoire en Python? car cette question traite principalement de l'augmentation de l'utilisation de la mémoire par rapport à la ligne de base, même après que l'interpréteur a libéré des objets via le ramasse-miettes (avec utilisation gc.collectou non).

Jared
la source
4
Il est à noter que ce comportement n'est pas spécifique à Python. Il arrive généralement que, lorsqu'un processus libère de la mémoire allouée au tas, la mémoire n'est pas libérée vers le système d'exploitation tant que le processus n'est pas mort.
NPE
Votre question pose plusieurs questions - dont certaines sont des dups, dont certaines sont inappropriées pour le SO, dont certaines pourraient être de bonnes questions. Vous demandez-vous si Python ne libère pas de mémoire, dans quelles circonstances exactes il peut / ne peut pas, quel est le mécanisme sous-jacent, pourquoi il a été conçu de cette façon, s'il existe des solutions de contournement ou autre chose?
abarnert
2
@abarnert J'ai combiné des sous-questions similaires. Pour répondre à vos questions: je sais que Python libère de la mémoire dans le système d'exploitation, mais pourquoi pas tout cela et pourquoi le montant qu'il fait. S'il y a des circonstances où il ne le peut pas, pourquoi? Quelles solutions de contournement aussi.
Jared
@jww je ne pense pas. Cette question était vraiment liée à la raison pour laquelle le processus d'interprétation n'a jamais libéré de mémoire même après avoir complètement collecté les déchets avec des appels à gc.collect.
Jared

Réponses:

86

La mémoire allouée sur le tas peut être sujette à des marques de marée haute. Ceci est compliqué par les optimisations internes de Python pour l'allocation de petits objets ( PyObject_Malloc) dans 4 pools de KiB, classés pour des tailles d'allocation à des multiples de 8 octets - jusqu'à 256 octets (512 octets en 3.3). Les pools eux-mêmes sont dans des arènes de 256 Kio, donc si un seul bloc dans un pool est utilisé, toute l'arène de 256 Kio ne sera pas libérée. Dans Python 3.3, l'allocateur de petits objets a été basculé vers l'utilisation de mappages de mémoire anonymes au lieu du tas, il devrait donc mieux fonctionner pour libérer de la mémoire.

En outre, les types intégrés conservent des listes libres d'objets précédemment alloués qui peuvent ou non utiliser l'allocateur de petits objets. Le inttype maintient une liste libre avec sa propre mémoire allouée, et l'effacer nécessite un appel PyInt_ClearFreeList(). Cela peut être appelé indirectement en faisant un full gc.collect.

Essayez-le comme ça et dites-moi ce que vous obtenez. Voici le lien pour psutil.Process.memory_info .

import os
import gc
import psutil

proc = psutil.Process(os.getpid())
gc.collect()
mem0 = proc.get_memory_info().rss

# create approx. 10**7 int objects and pointers
foo = ['abc' for x in range(10**7)]
mem1 = proc.get_memory_info().rss

# unreference, including x == 9999999
del foo, x
mem2 = proc.get_memory_info().rss

# collect() calls PyInt_ClearFreeList()
# or use ctypes: pythonapi.PyInt_ClearFreeList()
gc.collect()
mem3 = proc.get_memory_info().rss

pd = lambda x2, x1: 100.0 * (x2 - x1) / mem0
print "Allocation: %0.2f%%" % pd(mem1, mem0)
print "Unreference: %0.2f%%" % pd(mem2, mem1)
print "Collect: %0.2f%%" % pd(mem3, mem2)
print "Overall: %0.2f%%" % pd(mem3, mem0)

Production:

Allocation: 3034.36%
Unreference: -752.39%
Collect: -2279.74%
Overall: 2.23%

Éditer:

Je suis passé à la mesure par rapport à la taille de la machine virtuelle du processus pour éliminer les effets des autres processus du système.

Le runtime C (par exemple, glibc, msvcrt) réduit le tas lorsque l'espace libre contigu en haut atteint un seuil constant, dynamique ou configurable. Avec la glibc, vous pouvez régler cela avec mallopt(M_TRIM_THRESHOLD). Compte tenu de cela, il n'est pas surprenant que le tas diminue de plus - voire de beaucoup plus - que le bloc que vous free.

Dans 3.x rangene crée pas de liste, donc le test ci-dessus ne créera pas 10 millions d' intobjets. Même si c'était le cas, le inttype dans 3.x est fondamentalement un 2.x long, qui n'implémente pas de liste freelist.

Eryk Sun
la source
Utiliser à la memory_info()place de get_memory_info()et xest défini
Aziz Alto
Vous obtenez 10 ^ 7 ints même en Python 3, mais chacun remplace le dernier de la variable de boucle afin qu'ils n'existent pas tous en même temps.
Davis Herring
J'ai rencontré un problème de fuite de mémoire, et je devine la raison pour laquelle vous avez répondu ici. Mais comment puis-je prouver ma supposition? Existe-t-il un outil qui peut montrer que de nombreuses piscines sont malléables, mais qu'un petit bloc est utilisé?
ruiruige1991
130

Je suppose que la question qui vous tient vraiment à cœur ici est:

Existe-t-il un moyen de forcer Python à libérer toute la mémoire utilisée (si vous savez que vous n'utiliserez plus autant de mémoire)?

Non, il n'y en a pas. Mais il existe une solution simple: les processus enfants.

Si vous avez besoin de 500 Mo de stockage temporaire pendant 5 minutes, mais que vous devez ensuite exécuter pendant 2 heures supplémentaires et que vous ne toucherez plus jamais autant de mémoire, créez un processus enfant pour effectuer le travail gourmand en mémoire. Lorsque le processus enfant disparaît, la mémoire est libérée.

Ce n'est pas complètement trivial et gratuit, mais c'est assez facile et bon marché, ce qui est généralement assez bon pour que le commerce en vaille la peine.

Tout d'abord, le moyen le plus simple de créer un processus enfant est avec concurrent.futures(ou, pour 3.1 et futuresversions antérieures, le backport sur PyPI):

with concurrent.futures.ProcessPoolExecutor(max_workers=1) as executor:
    result = executor.submit(func, *args, **kwargs).result()

Si vous avez besoin d'un peu plus de contrôle, utilisez le multiprocessingmodule.

Les coûts sont:

  • Le démarrage du processus est un peu lent sur certaines plates-formes, notamment Windows. Nous parlons de millisecondes ici, pas de minutes, et si vous faites tourner un enfant pour faire 300 secondes de travail, vous ne le remarquerez même pas. Mais ce n'est pas gratuit.
  • Si la grande quantité de mémoire temporaire que vous utilisez est vraiment importante , cela peut entraîner le remplacement de votre programme principal. Bien sûr, vous gagnez du temps sur le long terme, car si cette mémoire traînait indéfiniment, cela devrait conduire à un échange à un moment donné. Mais cela peut transformer une lenteur graduelle en retards tout à la fois (et précoces) très visibles dans certains cas d'utilisation.
  • L'envoi de grandes quantités de données entre les processus peut être lent. Encore une fois, si vous parlez d'envoyer plus de 2K d'arguments et de récupérer 64K de résultats, vous ne le remarquerez même pas, mais si vous envoyez et recevez de grandes quantités de données, vous voudrez utiliser un autre mécanisme (un fichier, mmapped ou autre; les API de mémoire partagée dans multiprocessing; etc.).
  • L'envoi de grandes quantités de données entre les processus signifie que les données doivent être pickleable (ou, si vous les collez dans un fichier ou une mémoire partagée, struct-able ou idéalement ctypes-able).
Abarnert
la source
Truc vraiment sympa, mais ne résout pas le problème: (Mais j'aime vraiment ça
ddofborg
32

eryksun a répondu à la question n ° 1, et j'ai répondu à la question n ° 3 (l'original n ° 4), mais répondons maintenant à la question n ° 2:

Pourquoi libère-t-il 50,5 Mo en particulier - sur quoi se base le montant libéré?

Ce sur quoi il est basé est, en fin de compte, toute une série de coïncidences à l'intérieur de Python et mallocqui sont très difficiles à prévoir.

Premièrement, selon la façon dont vous mesurez la mémoire, vous ne mesurerez peut-être que des pages réellement mappées en mémoire. Dans ce cas, à chaque fois qu'une page est permutée par le pager, la mémoire apparaîtra comme "libérée", même si elle n'a pas été libérée.

Ou vous pouvez mesurer les pages en cours d'utilisation, qui peuvent ou non compter les pages allouées mais jamais touchées (sur des systèmes qui surallouent de manière optimiste, comme Linux), les pages qui sont allouées mais balisées MADV_FREE, etc.

Si vous mesurez vraiment les pages allouées (ce qui n'est en fait pas une chose très utile à faire, mais cela semble être ce que vous demandez), et que les pages ont vraiment été désallouées, deux circonstances dans lesquelles cela peut arriver: Soit vous ' brkvous avez utilisé ou équivalent pour réduire le segment de données (très rare de nos jours), ou vous avez utilisé munmapou similaire pour libérer un segment mappé. (Il existe également en théorie une variante mineure de ce dernier, en ce sens qu'il existe des moyens de libérer une partie d'un segment mappé - par exemple, la voler avec MAP_FIXEDpour un MADV_FREEsegment que vous démappez immédiatement.)

Mais la plupart des programmes n'allouent pas directement les choses à partir des pages de mémoire; ils utilisent un mallocallocateur de style. Lorsque vous appelez free, l'allocateur ne peut libérer des pages sur le système d'exploitation que si vous êtes simplement freele dernier objet actif d'un mappage (ou dans les N dernières pages du segment de données). Il n'y a aucun moyen pour votre application de prévoir raisonnablement cela, ni même de détecter que cela s'est produit à l'avance.

CPython rend cela encore plus compliqué: il a un allocateur d'objet personnalisé à 2 niveaux au-dessus d'un allocateur de mémoire personnalisé malloc. (Voir les commentaires source pour une explication plus détaillée.) Et en plus de cela, même au niveau de l'API C, encore moins Python, vous ne contrôlez même pas directement le moment où les objets de niveau supérieur sont désalloués.

Alors, lorsque vous libérez un objet, comment savoir s'il va libérer de la mémoire dans le système d'exploitation? Eh bien, vous devez d'abord savoir que vous avez publié la dernière référence (y compris toutes les références internes que vous ne connaissiez pas), permettant au GC de la désallouer. (Contrairement à d'autres implémentations, au moins CPython désallouera un objet dès qu'il est autorisé à le faire.) Cela désalloue généralement au moins deux choses au niveau suivant (par exemple, pour une chaîne, vous libérez l' PyStringobjet et le tampon de chaîne ).

Si vous faites désallouer un objet, à savoir si cela provoque le niveau suivant de désaffecter un bloc de stockage d'objets, vous devez connaître l'état interne de l'allocateur d'objet, ainsi que la façon dont il est mis en œuvre. (Cela ne peut évidemment pas arriver à moins que vous ne désallouiez la dernière chose du bloc, et même dans ce cas, cela peut ne pas arriver.)

Si vous faites désallouer un bloc de stockage d'objets, de savoir si cela provoque un freeappel, vous devez connaître l'état interne de l'allocateur PyMem, ainsi que la façon dont il est mis en œuvre. (Encore une fois, vous devez désallouer le dernier bloc utilisé dans une mallocrégion ed, et même dans ce cas, cela peut ne pas arriver.)

Si vous faites free une mallocrégion ed, pour savoir si cela provoque un munmapou équivalent (ou brk), vous devez connaître l'état interne du malloc, ainsi que comment il est implémenté. Et celui-ci, contrairement aux autres, est très spécifique à la plate-forme. (Et encore une fois, vous devez généralement désallouer le dernier en cours d'utilisation mallocdans un mmapsegment, et même dans ce cas, cela peut ne pas arriver.)

Donc, si vous voulez comprendre pourquoi il est arrivé à libérer exactement 50,5 Mo, vous allez devoir le retracer de bas en haut. Pourquoi mallocavez-vous démappé 50,5 Mo de pages lorsque vous avez effectué ces un ou plusieurs freeappels (probablement un peu plus de 50,5 Mo)? Vous devrez lire votre plate-forme malloc, puis parcourir les différents tableaux et listes pour voir son état actuel. (Sur certaines plates-formes, il peut même utiliser des informations au niveau du système, ce qui est pratiquement impossible à capturer sans faire un instantané du système à inspecter hors ligne, mais heureusement, ce n'est généralement pas un problème.) Et puis vous devez faites la même chose aux 3 niveaux supérieurs.

Donc, la seule réponse utile à la question est «Parce que».

À moins que vous ne fassiez du développement à ressources limitées (par exemple, intégrées), vous n'avez aucune raison de vous soucier de ces détails.

Et si vous êtes en train de faire le développement des ressources limitées, sachant que ces détails ne sert à rien; vous devez pratiquement faire une analyse finale autour de tous ces niveaux et plus particulièrement de mmapla mémoire dont vous avez besoin au niveau de l'application (éventuellement avec un allocateur de zone simple, bien compris et spécifique à l'application entre les deux).

Abarnert
la source
2

Tout d'abord, vous voudrez peut-être installer des regards:

sudo apt-get install python-pip build-essential python-dev lm-sensors 
sudo pip install psutil logutils bottle batinfo https://bitbucket.org/gleb_zhulik/py3sensors/get/tip.tar.gz zeroconf netifaces pymdstat influxdb elasticsearch potsdb statsd pystache docker-py pysnmp pika py-cpuinfo bernhard
sudo pip install glances

Ensuite, exécutez-le dans le terminal!

glances

Dans votre code Python, ajoutez au début du fichier, ce qui suit:

import os
import gc # Garbage Collector

Après avoir utilisé la variable "Big" (par exemple: myBigVar) pour laquelle vous souhaitez libérer de la mémoire, écrivez dans votre code python ce qui suit:

del myBigVar
gc.collect()

Dans un autre terminal, exécutez votre code python et observez dans le terminal "glances", comment la mémoire est gérée dans votre système!

Bonne chance!

PS Je suppose que vous travaillez sur un système Debian ou Ubuntu

de20ce
la source