¿Cómo puedo liberar explícitamente memoria en Python?

388

Escribí un programa Python que actúa en un archivo de entrada grande para crear algunos millones de objetos que representan triángulos. El algoritmo es:

  1. leer un archivo de entrada
  2. procesar el archivo y crear una lista de triángulos, representados por sus vértices
  3. genera los vértices en el formato OFF: una lista de vértices seguida de una lista de triángulos. Los triángulos están representados por índices en la lista de vértices.

El requisito de OFF de que imprima la lista completa de vértices antes de imprimir los triángulos significa que tengo que mantener la lista de triángulos en la memoria antes de escribir la salida en el archivo. Mientras tanto, recibo errores de memoria debido al tamaño de las listas.

¿Cuál es la mejor manera de decirle a Python que ya no necesito algunos de los datos y que se pueden liberar?

Nathan Fellman
fuente
11
¿Por qué no imprimir los triángulos en un archivo intermedio y volver a leerlos cuando los necesite?
Alice Purcell
2
Esta pregunta podría ser sobre dos cosas muy diferentes. ¿Son esos errores del mismo proceso de Python ?
Charles Duffy

Respuestas:

456

De acuerdo con la documentación oficial de Python , puede forzar al recolector de basura a liberar memoria sin referencia gc.collect(). Ejemplo:

import gc
gc.collect()
Havenard
fuente
19
De todos modos, las cosas se recolectan con frecuencia, excepto en algunos casos inusuales, así que no creo que eso ayude mucho.
Lennart Regebro
24
En general, se debe evitar gc.collect (). El recolector de basura sabe cómo hacer su trabajo. Dicho esto, si el OP se encuentra en una situación en la que de repente está desasignando muchos objetos (como en millones), gc.collect puede resultar útil.
Jason Baker
165
En realidad, llamarse a gc.collect()sí mismo al final de un ciclo puede ayudar a evitar la fragmentación de la memoria, lo que a su vez ayuda a mantener el rendimiento. He visto que esto hace una diferencia significativa (~ 20% de tiempo de ejecución IIRC)
RobM
39
Estoy usando Python 3.6. Llamar gc.collect()después de cargar un marco de datos de pandas desde hdf5 (500k filas) redujo el uso de memoria de 1.7GB a 500MB
John
15
Necesito cargar y procesar varios arreglos numpy de 25GB en un sistema con memoria de 32GB. El uso del my_arrayseguido de gc.collect()después de procesar la matriz es la única forma en que se libera la memoria y mi proceso sobrevive para cargar la siguiente matriz.
David
113

Desafortunadamente (dependiendo de su versión y versión de Python), algunos tipos de objetos usan "listas libres" que son una optimización local ordenada pero que pueden causar fragmentación de la memoria, específicamente al hacer que cada vez más memoria "asignada" solo para objetos de cierto tipo y por lo tanto no está disponible para el "fondo general".

La única forma realmente confiable de garantizar que un uso grande pero temporal de la memoria DEBE devolver todos los recursos al sistema cuando se hace, es hacer que ese uso ocurra en un subproceso, lo que hace que el trabajo que consume mucha memoria finalice. En tales condiciones, el sistema operativo REALIZARÁ su trabajo y con mucho gusto reciclará todos los recursos que el subproceso puede haber consumido. Afortunadamente, el multiprocessingmódulo hace que este tipo de operación (que solía ser bastante difícil) no sea tan malo en las versiones modernas de Python.

En su caso de uso, parece que la mejor manera para que los subprocesos acumulen algunos resultados y, sin embargo, se aseguren de que esos resultados estén disponibles para el proceso principal es usar archivos semi-temporales (quiero decir, semi-temporal, NO el tipo de archivos que desaparecerá automáticamente cuando se cierre, solo archivos normales que elimine explícitamente cuando haya terminado con ellos).

Alex Martelli
fuente
31
Seguro que me gustaría ver un ejemplo trivial de esto.
Aaron Hall
3
Seriamente. Lo que dijo @ AaronHall.
Noob Saibot
17
@AaronHall Ejemplo trivial ahora disponible , utilizando en multiprocessing.Managerlugar de archivos para implementar el estado compartido.
user4815162342
48

La deldeclaración podría ser útil, pero IIRC no garantiza que libere la memoria . Los documentos están aquí ... y el por qué no se publica aquí .

He escuchado a personas en sistemas tipo Linux y Unix bifurcar un proceso de Python para hacer un trabajo, obtener resultados y luego matarlo.

Este artículo tiene notas sobre el recolector de basura Python, pero creo que la falta de control de memoria es la desventaja de la memoria administrada

Aiden Bell
fuente
¿IronPython y Jython serían otra opción para evitar este problema?
Esteban Küber
@voyager: No, no lo haría. Y tampoco lo haría ningún otro idioma, de verdad. El problema es que él lee grandes cantidades de datos en una lista, y los datos son demasiado grandes para la memoria.
Lennart Regebro
1
Probablemente sería peor con IronPython o Jython. En esos entornos, ni siquiera está garantizado que la memoria se libere si nada más contiene una referencia.
Jason Baker
@voyager, sí, porque la máquina virtual Java busca globalmente memoria libre. Para la JVM, Jython no es nada especial. Por otro lado, la JVM tiene su propia cuota de inconvenientes, por ejemplo, que debe declarar de antemano qué tan grande puede usar.
contrato del Prof. Falken incumplió
32

Python se recolecta basura, por lo que si reduce el tamaño de su lista, recuperará memoria. También puede usar la declaración "del" para deshacerse completamente de una variable:

biglist = [blah,blah,blah]
#...
del biglist
Ned Batchelder
fuente
18
Esto es y no es cierto. Si bien la disminución del tamaño de la lista permite recuperar la memoria, no hay garantía de cuándo ocurrirá esto.
user142350
3
No, pero generalmente ayudará. Sin embargo, como entiendo la pregunta aquí, el problema es que tiene que tener tantos objetos que se quede sin memoria antes de procesarlos todos, si los lee en una lista. Es poco probable que eliminar la lista antes de que termine el procesamiento sea una solución útil. ;)
Lennart Regebro
3
¿Una condición de poca memoria / falta de memoria no desencadenaría una "ejecución de emergencia" del recolector de basura?
Jeremy Friesner
44
¿biglist = [] liberará memoria?
neouyghur
3
sí, si la lista anterior no está referenciada por otra cosa.
Ned Batchelder
22

No puedes liberar memoria explícitamente. Lo que debe hacer es asegurarse de no mantener referencias a objetos. Luego se recolectará basura, liberando la memoria.

En su caso, cuando necesita listas grandes, normalmente necesita reorganizar el código, generalmente utilizando generadores / iteradores en su lugar. De esa manera, no necesita tener las listas grandes en la memoria.

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

Lennart Regebro
fuente
1
Si este enfoque es factible, entonces probablemente valga la pena hacerlo. Pero debe tenerse en cuenta que no puede hacer acceso aleatorio en iteradores, lo que puede causar problemas.
Jason Baker
Es cierto, y si es necesario, acceder a grandes conjuntos de datos de forma aleatoria probablemente requiera algún tipo de base de datos.
Lennart Regebro
Puede usar fácilmente un iterador para extraer un subconjunto aleatorio de otro iterador.
S.Lott
Es cierto, pero luego tendría que recorrer todo para obtener el subconjunto, que será muy lento.
Lennart Regebro
21

( delpuede ser su amigo, ya que marca los objetos como borrables cuando no hay otras referencias a ellos. Ahora, a menudo el intérprete de CPython guarda esta memoria para su uso posterior, por lo que su sistema operativo podría no ver la memoria "liberada").

Quizás, en primer lugar, no se encuentre con ningún problema de memoria utilizando una estructura más compacta para sus datos. Por lo tanto, las listas de números son mucho menos eficientes en memoria que el formato utilizado por el arraymódulo estándar o el numpymódulo de terceros . Ahorraría memoria colocando sus vértices en una matriz NumPy 3xN y sus triángulos en una matriz de N elementos.

Eric O Lebigot
fuente
Eh? La recolección de basura de CPython se basa en el recuento; no es un marcado y barrido periódico (como para muchas implementaciones JVM comunes), sino que elimina de inmediato algo en el momento en que su recuento de referencia llega a cero. Solo los ciclos (donde los reembolsos serían cero pero no se deben a bucles en el árbol de referencia) requieren mantenimiento periódico. delno hace nada que simplemente reasignar un valor diferente a todos los nombres que hacen referencia a un objeto no lo haría.
Charles Duffy
Veo de dónde vienes: actualizaré la respuesta en consecuencia. Entiendo que el intérprete de CPython en realidad funciona de alguna manera intermedia: dellibera la memoria desde el punto de vista de Python, pero generalmente no desde el punto de vista de la biblioteca de tiempo de ejecución C o del sistema operativo. Referencias: stackoverflow.com/a/32167625/4297 , effbot.org/pyfaq/… .
Eric O Lebigot
De acuerdo con el contenido de sus enlaces, pero suponiendo que el OP esté hablando de un error que obtienen del mismo proceso de Python , la distinción entre liberar memoria para el montón local del proceso y para el sistema operativo no parece ser relevante ( ya que liberar el montón hace que ese espacio esté disponible para nuevas asignaciones dentro de ese proceso de Python). Y para ello, deles igualmente eficaz con salidas-de-alcance, los cambios de destino, etc.
Charles Duffy
11

Tuve un problema similar al leer un gráfico de un archivo. El procesamiento incluyó el cálculo de una matriz flotante de 200 000x200 000 (una línea a la vez) que no cabía en la memoria. Tratar de liberar la memoria entre los cálculos utilizando gc.collect()el aspecto fijo del problema relacionado con la memoria, pero resultó en problemas de rendimiento: no sé por qué, pero a pesar de que la cantidad de memoria utilizada permaneció constante, cada nueva llamada gc.collect()tomó más tiempo que El anterior. Así que bastante rápido la recolección de basura tomó la mayor parte del tiempo de cálculo.

Para solucionar los problemas de memoria y rendimiento, cambié al uso de un truco de subprocesos múltiples que leí una vez en algún lugar (lo siento, ya no puedo encontrar la publicación relacionada). Antes de leer cada línea del archivo en un gran forbucle, procesarlo y ejecutarlo de gc.collect()vez en cuando para liberar espacio en la memoria. Ahora llamo a una función que lee y procesa un fragmento del archivo en un nuevo hilo. Una vez que finaliza el subproceso, la memoria se libera automáticamente sin el extraño problema de rendimiento.

Prácticamente funciona así:

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
fuente
1
Me pregunto por qué estás usando `//` `s en lugar de # en Python para comentarios.
JC Rocamonde
Me mezclé entre idiomas. Gracias por el comentario, actualicé la sintaxis.
Retzod
9

Otros han publicado algunas formas en que podría "convencer" al intérprete de Python para que libere la memoria (o evite tener problemas de memoria). Lo más probable es que primero pruebes sus ideas. Sin embargo, creo que es importante darle una respuesta directa a su pregunta.

Realmente no hay ninguna forma de decirle directamente a Python que libere memoria. El hecho es que si desea un nivel de control tan bajo, tendrá que escribir una extensión en C o C ++.

Dicho esto, hay algunas herramientas para ayudar con esto:

Jason Baker
fuente
3
gc.collect () y del gc.garbage [:] funcionan bien cuando uso grandes cantidades de memoria
Andrew Scott Evans
3

Si no le importa la reutilización de vértices, podría tener dos archivos de salida: uno para vértices y otro para triángulos. Luego agregue el archivo de triángulo al archivo de vértice cuando haya terminado.

Nosredna
fuente
1
Me imagino que solo puedo mantener los vértices en la memoria e imprimir los triángulos en un archivo, y luego imprimir los vértices solo al final. Sin embargo, el acto de escribir los triángulos en un archivo es una gran pérdida de rendimiento. ¿Hay alguna forma de acelerar eso ?
Nathan Fellman el