Comment puis-je explicitement libérer de la mémoire en Python?

389

J'ai écrit un programme Python qui agit sur un gros fichier d'entrée pour créer quelques millions d'objets représentant des triangles. L'algorithme est:

  1. lire un fichier d'entrée
  2. traiter le fichier et créer une liste de triangles, représentés par leurs sommets
  3. sortie des sommets au format OFF: une liste de sommets suivie d'une liste de triangles. Les triangles sont représentés par des indices dans la liste des sommets

L'exigence de OFF que j'imprime la liste complète des sommets avant d'imprimer les triangles signifie que je dois conserver la liste des triangles en mémoire avant d'écrire la sortie dans un fichier. En attendant, je reçois des erreurs de mémoire en raison de la taille des listes.

Quelle est la meilleure façon de dire à Python que je n'ai plus besoin de certaines données et qu'elles peuvent être libérées?

Nathan Fellman
la source
11
Pourquoi ne pas imprimer les triangles dans un fichier intermédiaire et les relire lorsque vous en avez besoin?
Alice Purcell
2
Cette question pourrait potentiellement concerner deux choses bien différentes. Ces erreurs proviennent-elles du même processus Python , auquel cas nous nous soucions de libérer de la mémoire dans le tas du processus Python, ou proviennent-elles de processus différents sur le système, auquel cas nous nous soucions de libérer de la mémoire sur le système d'exploitation?
Charles Duffy

Réponses:

456

Selon la documentation officielle de Python , vous pouvez forcer le garbage collector à libérer de la mémoire non référencée avec gc.collect(). Exemple:

import gc
gc.collect()
Havenard
la source
19
De toute façon, les choses sont souvent ramassées, sauf dans certains cas inhabituels, donc je ne pense pas que cela aidera beaucoup.
Lennart Regebro
24
En général, gc.collect () est à éviter. Le garbage collector sait faire son travail. Cela dit, si l'OP se trouve dans une situation où il désalloue soudainement beaucoup d'objets (comme des millions), gc.collect peut s'avérer utile.
Jason Baker
165
En fait, gc.collect()vous appeler à la fin d'une boucle peut éviter de fragmenter la mémoire, ce qui contribue à maintenir les performances. J'ai vu cela faire une différence significative (~ 20% d'exécution IIRC)
RobM
39
J'utilise python 3.6. L'appel gc.collect()après le chargement d'une trame de données pandas à partir de hdf5 (500 000 lignes) a réduit l'utilisation de la mémoire de 1,7 Go à 500 Mo
John
15
J'ai besoin de charger et de traiter plusieurs tableaux numpy de 25 Go dans un système avec 32 Go de mémoire. L'utilisation del my_arraysuivie par gc.collect()après le traitement du tableau est le seul moyen de libérer la mémoire et mon processus survit pour charger le tableau suivant.
David
113

Malheureusement (en fonction de votre version et de la version de Python) certains types d'objets utilisent des "listes gratuites" qui sont une optimisation locale soignée mais peuvent provoquer une fragmentation de la mémoire, en particulier en faisant de plus en plus de mémoire "réservée" uniquement aux objets d'un certain type et ainsi indisponible pour le "fonds général".

La seule manière vraiment fiable de garantir qu'une utilisation importante mais temporaire de la mémoire renvoie toutes les ressources au système une fois terminée, est de faire en sorte que cette utilisation se produise dans un sous-processus, ce qui fait que le travail gourmand en mémoire se termine. Dans de telles conditions, le système d'exploitation fera son travail et recyclera volontiers toutes les ressources que le sous-processus peut avoir englouties. Heureusement, le multiprocessingmodule rend ce genre d'opération (qui était plutôt pénible) pas trop mal dans les versions modernes de Python.

Dans votre cas d'utilisation, il semble que la meilleure façon pour les sous-processus d'accumuler certains résultats tout en s'assurant que ces résultats sont disponibles pour le processus principal est d'utiliser des fichiers semi-temporaires (par semi-temporaires, je veux dire, PAS le type de fichiers qui disparaissent automatiquement lorsqu'ils sont fermés, seuls les fichiers ordinaires que vous supprimez explicitement lorsque vous en avez terminé).

Alex Martelli
la source
31
Je voudrais certainement voir un exemple trivial de cela.
Aaron Hall
3
Sérieusement. Ce que @AaronHall a dit.
Noob Saibot
17
@AaronHall Exemple trivial maintenant disponible , utilisant multiprocessing.Managerplutôt que des fichiers pour implémenter l'état partagé.
user4815162342
48

La deldéclaration pourrait être utile, mais IIRC il n'est pas garanti de libérer la mémoire . Les documents sont ici ... et une raison pour laquelle il n'est pas publié est ici .

J'ai entendu des gens sur des systèmes de type Linux et Unix bifurquer un processus python pour faire du travail, obtenir des résultats puis le tuer.

Cet article contient des notes sur le garbage collector Python, mais je pense que le manque de contrôle de la mémoire est l'inconvénient de la mémoire gérée

Aiden Bell
la source
IronPython et Jython seraient-ils une autre option pour éviter ce problème?
Esteban Küber
@voyager: Non, ce ne serait pas le cas. Et aucune autre langue ne le serait vraiment. Le problème est qu'il lit de grandes quantités de données dans une liste et que les données sont trop volumineuses pour la mémoire.
Lennart Regebro
1
Ce serait probablement pire sous IronPython ou Jython. Dans ces environnements, vous n'êtes même pas assuré que la mémoire sera libérée si rien d'autre ne contient une référence.
Jason Baker
@voyager, oui, car la machine virtuelle Java recherche globalement la mémoire à libérer. Pour la JVM, Jython n'a rien de spécial. D'un autre côté, la JVM a son propre lot d'inconvénients, par exemple, vous devez déclarer à l'avance la taille du tas qu'elle peut utiliser.
contrat du professeur Falken a été rompu
32

Python est récupéré, si vous réduisez la taille de votre liste, il récupérera de la mémoire. Vous pouvez également utiliser l'instruction "del" pour vous débarrasser complètement d'une variable:

biglist = [blah,blah,blah]
#...
del biglist
Ned Batchelder
la source
18
C'est et ce n'est pas vrai. Bien que la diminution de la taille de la liste permette de récupérer la mémoire, rien ne garantit quand cela se produira.
user142350
3
Non, mais en général, cela aidera. Cependant, si je comprends bien la question ici, le problème est qu'il doit avoir tellement d'objets qu'il manque de mémoire avant de les traiter tous, s'il les lit dans une liste. Il est peu probable que la suppression de la liste avant la fin du traitement soit une solution utile. ;)
Lennart Regebro
3
Une condition de mémoire insuffisante / insuffisante ne déclencherait-elle pas une «exécution d'urgence» du garbage collector?
Jeremy Friesner
4
biglist = [] libérera-t-il de la mémoire?
neouyghur
3
oui, si l'ancienne liste n'est référencée par rien d'autre.
Ned Batchelder
22

Vous ne pouvez pas libérer explicitement de la mémoire. Ce que vous devez faire est de vous assurer de ne pas conserver de références aux objets. Ils seront ensuite récupérés, libérant ainsi la mémoire.

Dans votre cas, lorsque vous avez besoin de grandes listes, vous devez généralement réorganiser le code, en utilisant généralement des générateurs / itérateurs à la place. De cette façon, vous n'avez pas du tout besoin d'avoir les grandes listes en mémoire.

http://www.prasannatech.net/2009/07/introduction-python-generators.html

Lennart Regebro
la source
1
Si cette approche est réalisable, cela vaut probablement la peine. Mais il convient de noter que vous ne pouvez pas effectuer d'accès aléatoire sur les itérateurs, ce qui peut provoquer des problèmes.
Jason Baker
C'est vrai, et si cela est nécessaire, l'accès aléatoire à de grands ensembles de données nécessitera probablement une sorte de base de données.
Lennart Regebro
Vous pouvez facilement utiliser un itérateur pour extraire un sous-ensemble aléatoire d'un autre itérateur.
S.Lott
Certes, mais vous devrez ensuite parcourir tout pour obtenir le sous-ensemble, qui sera très lent.
Lennart Regebro
21

( delpeut être votre ami, car il marque les objets comme pouvant être supprimés lorsqu'il n'y a aucune autre référence à eux. Maintenant, souvent, l'interpréteur CPython conserve cette mémoire pour une utilisation ultérieure, de sorte que votre système d'exploitation peut ne pas voir la mémoire "libérée".)

Peut-être que vous ne rencontriez pas de problème de mémoire en premier lieu en utilisant une structure plus compacte pour vos données. Ainsi, les listes de nombres sont beaucoup moins efficaces en mémoire que le format utilisé par le arraymodule standard ou le numpymodule tiers . Vous économiseriez de la mémoire en plaçant vos sommets dans un tableau NumPy 3xN et vos triangles dans un tableau à N éléments.

Eric O Lebigot
la source
Eh? Le garbage collection de CPython est basé sur un nouveau comptage; ce n'est pas un marquage et un balayage périodiques (comme pour de nombreuses implémentations JVM courantes), mais au lieu de cela, il supprime immédiatement quelque chose au moment où son nombre de références atteint zéro. Seuls les cycles (où les refcounts seraient nuls mais ne le sont pas à cause des boucles dans l'arbre de référence) nécessitent une maintenance périodique. delne fait rien qui ne réattribue simplement une valeur différente à tous les noms référençant un objet.
Charles Duffy
Je vois d'où vous venez: je mettrai à jour la réponse en conséquence. Je comprends que l'interpréteur CPython fonctionne en fait d'une manière intermédiaire: dellibère la mémoire du point de vue de Python, mais généralement pas du point de vue de la bibliothèque d'exécution C ou du système d'exploitation. Références: stackoverflow.com/a/32167625/4297 , effbot.org/pyfaq/… .
Eric O Lebigot
D'accord quant au contenu de vos liens, mais en supposant que l'OP parle d'une erreur qu'ils obtiennent du même processus Python , la distinction entre libérer de la mémoire vers le tas local du processus et vers le système d'exploitation ne semble pas susceptible d'être pertinente ( car la libération sur le tas rend cet espace disponible pour de nouvelles allocations dans ce processus Python). Et pour cela, delest tout aussi efficace avec les sorties hors champ, les réaffectations, etc.
Charles Duffy
11

J'ai eu un problème similaire en lisant un graphique à partir d'un fichier. Le traitement comprenait le calcul d'une matrice flottante de 200 000 x 200 000 (une ligne à la fois) qui ne tenait pas en mémoire. Essayer de libérer de la mémoire entre les calculs en utilisant gc.collect()fixe l'aspect lié à la mémoire du problème, mais cela a entraîné des problèmes de performances: je ne sais pas pourquoi, mais même si la quantité de mémoire utilisée est restée constante, chaque nouvel appel a gc.collect()pris plus de temps que le précédent. Donc, assez rapidement, la collecte des ordures a pris la plupart du temps de calcul.

Pour résoudre à la fois les problèmes de mémoire et de performances, je suis passé à l'utilisation d'une astuce multithreading que j'ai lue une fois quelque part (je suis désolé, je ne trouve plus le message correspondant). Avant, je lisais chaque ligne du fichier dans une grande forboucle, le traitais et l'exécutais de gc.collect()temps en temps pour libérer de l'espace mémoire. Maintenant, j'appelle une fonction qui lit et traite une partie du fichier dans un nouveau thread. Une fois le thread terminé, la mémoire est automatiquement libérée sans l'étrange problème de performances.

Pratiquement, cela fonctionne comme ceci:

from dask import delayed  # this module wraps the multithreading
def f(storage, index, chunk_size):  # the processing function
    # read the chunk of size chunk_size starting at index in the file
    # process it using data in storage if needed
    # append data needed for further computations  to storage 
    return storage

partial_result = delayed([])  # put into the delayed() the constructor for your data structure
# I personally use "delayed(nx.Graph())" since I am creating a networkx Graph
chunk_size = 100  # ideally you want this as big as possible while still enabling the computations to fit in memory
for index in range(0, len(file), chunk_size):
    # we indicates to dask that we will want to apply f to the parameters partial_result, index, chunk_size
    partial_result = delayed(f)(partial_result, index, chunk_size)

    # no computations are done yet !
    # dask will spawn a thread to run f(partial_result, index, chunk_size) once we call partial_result.compute()
    # passing the previous "partial_result" variable in the parameters assures a chunk will only be processed after the previous one is done
    # it also allows you to use the results of the processing of the previous chunks in the file if needed

# this launches all the computations
result = partial_result.compute()

# one thread is spawned for each "delayed" one at a time to compute its result
# dask then closes the tread, which solves the memory freeing issue
# the strange performance issue with gc.collect() is also avoided
Retzod
la source
1
Je me demande pourquoi vous utilisez `//` `au lieu de # en Python pour les commentaires.
JC Rocamonde
Je me suis mélangé entre les langues. Merci pour la remarque, j'ai mis à jour la syntaxe.
Retzod
9

D'autres ont publié des moyens de "persuader" l'interpréteur Python de libérer la mémoire (ou d'éviter autrement des problèmes de mémoire). Il y a de fortes chances que vous essayiez d'abord leurs idées. Cependant, je pense qu'il est important de vous donner une réponse directe à votre question.

Il n'y a vraiment aucun moyen de dire directement à Python de libérer de la mémoire. Le fait est que si vous voulez un niveau de contrôle aussi bas, vous devrez écrire une extension en C ou C ++.

Cela dit, il existe des outils pour vous aider:

Jason Baker
la source
3
gc.collect () et del gc.garbage [:] fonctionnent très bien lorsque j'utilise de grandes quantités de mémoire
Andrew Scott Evans
3

Si vous ne vous souciez pas de la réutilisation des sommets, vous pouvez avoir deux fichiers de sortie - un pour les sommets et un pour les triangles. Ajoutez ensuite le fichier triangle au fichier vertex lorsque vous avez terminé.

Nosredna
la source
1
Je suppose que je ne peux garder que les sommets en mémoire et imprimer les triangles dans un fichier, puis imprimer les sommets uniquement à la fin. Cependant, le fait d'écrire les triangles dans un fichier est une énorme perte de performances. Y a-t-il un moyen d'accélérer cela ?
Nathan Fellman