web-dev-qa-db-fra.com

Libérer de la mémoire dans Python

J'ai quelques questions connexes concernant 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 monte à 80.9mb. Alors je

    del foo
    

    la mémoire réelle diminue, mais uniquement pour 30.4mb. L'interprète utilise 4.4mb base, alors quel est l'avantage de ne pas libérer 26mb de la mémoire sur le système d'exploitation? Est-ce parce que Python est en train de "planifier à l'avance", pensant que vous pourriez utiliser à nouveau autant de mémoire?

  2. Pourquoi publie-t-il 50.5mb en particulier - sur quoi repose 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)?

NOTE Cette question est différente de Comment puis-je explicitement libérer de la mémoire en Python? car cette question concerne principalement l'augmentation de l'utilisation de la mémoire depuis la ligne de base même après que l'interprète ait libéré des objets via le garbage collection (avec l'utilisation de gc.collect ou non).

118
Jared

La mémoire allouée sur le tas peut être sujette aux hautes eaux. Cela est compliqué par les optimisations internes de Python pour l'allocation de petits objets (PyObject_Malloc) dans 4 pools KiB, classés pour des tailles d'allocation de multiples de 8 octets - jusqu'à 256 octets (512 octets en 3.3). Les pools eux-mêmes sont situés dans des arènes de 256 Ko. Par conséquent, si un seul bloc est utilisé dans un pool, l’ensemble de l’arène de 256 Ko ne sera pas publié. Dans Python 3.3, l’allocateur de petit objet a été remplacé par l’utilisation de mappes de mémoire anonymes au lieu du tas, de sorte qu’il devrait mieux libérer de la mémoire.

En outre, les types intégrés gèrent des listes indépendantes d'objets alloués précédemment qui peuvent ou non utiliser l'allocateur de petit objet. Le type int maintient une liste libre avec sa propre mémoire allouée et son effacement nécessite d’appeler PyInt_ClearFreeList(). Ceci peut être appelé indirectement en faisant un gc.collect complet.

Essayez 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)

Sortie:

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

Modifier:

Je suis passé à la mesure relative à la taille du processus VM 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 au sommet atteint un seuil constant, dynamique ou configurable. Avec la glibc, vous pouvez régler ceci avec mallopt (M_TRIM_THRESHOLD). Compte tenu de cela, il n’est pas surprenant que le tas diminue de plus, voire beaucoup plus, que le bloc que vous avez free.

Dans 3.x range ne crée pas de liste, le test ci-dessus ne créera pas 10 millions d'objets int. Même si c'était le cas, le type int dans 3.x est fondamentalement un 2.x long, qui n'implémente pas une liste libre.

81
Eryk Sun

Je suppose que la question qui vous tient à coeur est la suivante:

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 de contournement simple: les processus enfants.

Si vous avez besoin de 500 Mo de stockage temporaire pendant 5 minutes, mais que vous n’ayez pas besoin de trop de mémoire, lancez un processus enfant pour effectuer le travail exigeant en mémoire. Lorsque le processus enfant s'en va, la mémoire est libérée.

Ce n’est pas totalement gratuit et gratuit, mais c’est assez facile et bon marché, ce qui est généralement suffisant pour que le commerce en vaille la peine.

Premièrement, le moyen le plus simple de créer un processus enfant consiste à utiliser concurrent.futures (ou, pour 3.1 et les versions antérieures, le futures 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 module multiprocessing .

Les coûts sont:

  • Le démarrage des processus est un peu lent sur certaines plates-formes, notamment Windows. Nous parlons ici en millisecondes, pas en minutes, et si vous amenez un enfant à 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 réellement volumineuse , votre programme principal risque alors d'être échangé. Bien sûr, vous gagnez du temps sur le long terme, car si cette mémoire restait indéfiniment, elle devrait conduire à un échange à un moment donné. Cela peut toutefois transformer les lenteurs graduelles en retards très visibles, tous en une fois (et précoces) dans certains cas d'utilisation.
  • L'envoi de grandes quantités de données entre processus peut être lent. Encore une fois, si vous parlez d'envoyer plus de 2 000 arguments et de récupérer 64 000 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 processus signifie que les données doivent pouvoir être traitées (ou, si vous les collez dans un fichier ou dans une mémoire partagée, struct- ou idéalement ctypes-).
120
abarnert

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 repose le montant libéré?

Il repose sur toute une série de coïncidences à l'intérieur de Python et malloc qui sont très difficiles à prédire.

Premièrement, selon la façon dont vous mesurez la mémoire, il se peut que vous ne mesuriez que les pages réellement mappées dans la mémoire. Dans ce cas, chaque fois qu'une page est remplacée par le pageur, la mémoire apparaîtra comme "libérée", même si elle n'a pas été libérée.

Vous pouvez également mesurer des pages en cours d'utilisation, qui peuvent ou non compter des pages allouées mais jamais touchées (sur des systèmes optimisant l'attribution excessive, comme Linux), des pages allouées mais marquées MADV_FREE, etc. .

Si vous mesurez réellement 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 se produire: Soit vous ' Nous avons utilisé brk ou l’équivalent pour rétrécir le segment de données (très rare de nos jours), ou vous avez utilisé munmap ou un logiciel similaire pour libérer un segment mappé. (Il existe aussi théoriquement une variante mineure à cette dernière, dans la mesure où il existe des moyens de libérer une partie d'un segment mappé - par exemple, volez-la avec MAP_FIXED pour un segment MADV_FREE que vous annulez immédiatement.

Mais la plupart des programmes n'allouent pas directement des pages hors mémoire. ils utilisent un allocateur de style malloc-. Lorsque vous appelez free, l’allocateur ne peut libérer des pages dans le système d’exploitation que si vous êtes simplement freeing le dernier objet en direct d’un mappage (ou dans les N dernières pages du segment de données). Il n’ya aucun moyen que votre application puisse raisonnablement prédire cela, ou même détecter que cela s’est produit à l’avance.

CPython rend cela encore plus compliqué - il a un allocateur d'objet personnalisé à 2 niveaux qui s'ajoute à un allocateur de mémoire personnalisé et à malloc. (Voir les commentaires sources 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é.

Ainsi, lorsque vous relâchez un objet, comment savoir s'il libère 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 les références internes que vous ne connaissiez pas), ce qui permet au GC de la désallouer. (Contrairement à d'autres implémentations, au moins CPython libérera un objet dès qu'il sera 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'objet PyString , et le tampon de chaîne).

Si vous libérez un objet , pour savoir si cela provoque le passage au niveau suivant pour libérer 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 pourrait ne pas arriver.)

Si vous libérez un bloc de stockage d’objets, pour savoir si cela provoque un appel free, vous devez connaître l’état interne du PyMem allocator, ainsi que son implémentation. (Encore une fois, vous devez désallouer le dernier bloc en cours d’utilisation dans une région affectée malloc, et même dans ce cas, il se peut que cela ne se produise pas.)

Si vous faites free une région _variable malloced, savoir si cela provoque un munmap ou un équivalent (ou brk), vous devez connaître l'état interne de malloc, ainsi que son implémentation. 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 malloc en cours d’utilisation dans un segment mmap, et même dans ce cas, il se peut que cela ne se produise pas.)

Donc, si vous voulez comprendre pourquoi il est arrivé de libérer exactement 50,5 Mo, vous devrez le retracer de bas en haut. Pourquoi malloc a-t-il dissocié 50,5 Mo de pages alors que vous utilisiez ces un ou plusieurs appels free (probablement un peu plus de 50,5 Mo)? Vous devez lire le malloc de votre plate-forme, puis parcourir les différentes tables 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 quasiment impossible à capturer sans prendre un instantané du système pour l'inspecter hors ligne, mais heureusement, cela ne pose généralement pas de problème.) Et ensuite, vous devez faire la même chose aux 3 niveaux supérieurs.

Ainsi, la seule réponse utile à la question est "Parce que".

Sauf si vous effectuez un développement limité en ressources (intégré, par exemple), vous n'avez aucune raison de vous soucier de ces détails.

Et si vous effectuez un développement dont les ressources sont limitées, connaître ces détails est inutile; vous devez effectuer une analyse finale de tous ces niveaux et en particulier de mmap de la 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).

30
abarnert

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

Puis lancez-le dans le terminal!

glances

Dans votre code Python, ajoutez au début du fichier les éléments suivants:

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:

del myBigVar
gc.collect()

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

Bonne chance!

P.S. Je suppose que vous travaillez sur un système Debian ou Ubuntu

1
de20ce