Liberando memoria en Python

128

Tengo algunas preguntas relacionadas con el uso de memoria en el siguiente ejemplo.

  1. Si corro en el intérprete,

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

    la memoria real utilizada en mi máquina aumenta 80.9mb. Entonces yo,

    del foo

    la memoria real se cae, pero solo a 30.4mb. El intérprete utiliza la 4.4mblínea de base, ¿cuál es la ventaja de no liberar 26mbmemoria al sistema operativo? ¿Es porque Python está "planeando con anticipación", pensando que puede volver a usar tanta memoria?

  2. ¿Por qué se libera 50.5mben particular? ¿En qué cantidad se libera según?

  3. ¿Hay alguna manera de obligar a Python a liberar toda la memoria que se usó (si sabe que no volverá a usar tanta memoria)?

NOTA Esta pregunta es diferente de ¿Cómo puedo liberar memoria explícitamente en Python? porque esta pregunta trata principalmente con el aumento del uso de memoria desde la línea de base incluso después de que el intérprete haya liberado objetos mediante la recolección de basura (con uso gc.collecto no).

Jared
fuente
44
Vale la pena señalar que este comportamiento no es específico de Python. En general, cuando un proceso libera algo de memoria asignada en el montón, la memoria no se devuelve al sistema operativo hasta que el proceso muere.
NPE
Su pregunta plantea múltiples cosas, algunas de las cuales son dups, algunas de las cuales son inapropiadas para SO, algunas de las cuales podrían ser buenas preguntas. ¿Está preguntando si Python no libera memoria, en qué circunstancias exactas puede / no puede, cuál es el mecanismo subyacente, por qué fue diseñado de esa manera, si hay soluciones alternativas o algo completamente diferente?
abarnert
2
@abarnert Combiné preguntas secundarias similares. Para responder a sus preguntas: Sé que Python libera algo de memoria para el sistema operativo, pero por qué no todo y por qué la cantidad que hace. Si hay circunstancias en las que no puede, ¿por qué? Qué soluciones también.
Jared
@jww No lo creo. Esta pregunta realmente relacionado con la memoria por qué el proceso intérprete nunca lanzó incluso después de la recogida de basura totalmente con llamadas a gc.collect.
Jared

Respuestas:

86

La memoria asignada en el montón puede estar sujeta a marcas altas. Esto se complica por las optimizaciones internas de Python para asignar objetos pequeños ( PyObject_Malloc) en 4 grupos de KiB, clasificados para tamaños de asignación en múltiplos de 8 bytes, hasta 256 bytes (512 bytes en 3.3). Los grupos en sí están en arenas de 256 KiB, por lo que si solo se usa un bloque en un grupo, no se liberará todo el escenario de 256 KiB. En Python 3.3, el asignador de objetos pequeños se cambió a mapas de memoria anónimos en lugar del montón, por lo que debería funcionar mejor al liberar memoria.

Además, los tipos integrados mantienen listas gratuitas de objetos asignados previamente que pueden o no usar el asignador de objetos pequeños. El inttipo mantiene una lista libre con su propia memoria asignada, y para borrarla es necesario llamar PyInt_ClearFreeList(). Esto se puede llamar indirectamente haciendo un completo gc.collect.

Pruébalo así y dime qué obtienes. Aquí está el enlace para 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)

Salida:

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

Editar:

Cambié a medir en relación con el tamaño de VM del proceso para eliminar los efectos de otros procesos en el sistema.

El tiempo de ejecución C (por ejemplo, glibc, msvcrt) reduce el montón cuando el espacio libre contiguo en la parte superior alcanza un umbral constante, dinámico o configurable. Con glibc puede ajustar esto con mallopt(M_TRIM_THRESHOLD). Dado esto, no es sorprendente que el montón se reduzca más, incluso mucho más que el bloque que usted free.

En 3.x rangeno se crea una lista, por lo que la prueba anterior no creará 10 millones de intobjetos. Incluso si lo hiciera, el inttipo en 3.x es básicamente un 2.x long, que no implementa una lista libre.

Eryk Sun
fuente
Uso en memory_info()lugar de get_memory_info()y xestá definido
Aziz Alto
Obtiene 10 ^ 7 ints incluso en Python 3, pero cada uno reemplaza al último en la variable de bucle para que no existan todos a la vez.
Davis Herring
Me he encontrado con un problema de pérdida de memoria, y supongo que la razón es lo que has respondido aquí. Pero, ¿cómo puedo probar mi suposición? ¿Existe alguna herramienta que pueda mostrar que muchos grupos están mal colocados, pero solo se usa un pequeño bloque?
ruiruige1991
130

Supongo que la pregunta que realmente te importa aquí es:

¿Hay alguna manera de obligar a Python a liberar toda la memoria que se usó (si sabe que no volverá a usar tanta memoria)?

No no hay. Pero hay una solución fácil: procesos secundarios.

Si necesita 500 MB de almacenamiento temporal durante 5 minutos, pero después de eso debe ejecutar otras 2 horas y no volverá a tocar tanta memoria, genere un proceso secundario para realizar el trabajo intensivo en memoria. Cuando el proceso secundario desaparece, la memoria se libera.

Esto no es completamente trivial y gratuito, pero es bastante fácil y barato, lo que generalmente es lo suficientemente bueno como para que el intercambio valga la pena.

Primero, la forma más fácil de crear un proceso hijo es con concurrent.futures(o, para 3.1 y futuresversiones anteriores, el backport en PyPI):

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

Si necesita un poco más de control, use el multiprocessingmódulo.

Los costos son:

  • El inicio del proceso es algo lento en algunas plataformas, especialmente en Windows. Estamos hablando de milisegundos aquí, no minutos, y si estás haciendo girar a un niño para hacer un trabajo de 300 segundos, ni siquiera lo notarás. Pero no es gratis.
  • Si la gran cantidad de memoria temporal que usa realmente es grande , esto puede hacer que su programa principal se intercambie. Por supuesto, está ahorrando tiempo a largo plazo, porque si esa memoria perdurara para siempre, tendría que conducir al intercambio en algún momento. Pero esto puede convertir la lentitud gradual en retrasos muy notables de forma simultánea (y temprana) en algunos casos de uso.
  • El envío de grandes cantidades de datos entre procesos puede ser lento. Nuevamente, si está hablando de enviar más de 2K de argumentos y recuperar 64K de resultados, ni siquiera lo notará, pero si envía y recibe grandes cantidades de datos, querrá usar algún otro mecanismo (un archivo, mmapped u otro; las API de memoria compartida en multiprocessing; etc.).
  • El envío de grandes cantidades de datos entre procesos significa que los datos tienen que ser seleccionables (o, si los pega en un archivo o memoria compartida, structpueden ser o pueden ser ideales ctypes).
abarnert
fuente
Realmente buen truco, aunque no resolvió el problema :( Pero realmente me gusta
ddofborg
32

eryksun ha respondido la pregunta n. ° 1, y he respondido la pregunta n. ° 3 (la original n. ° 4), pero ahora respondamos la pregunta n. ° 2:

¿Por qué libera 50.5mb en particular? ¿En qué cantidad se libera?

En lo que se basa es, en última instancia, en toda una serie de coincidencias dentro de Python y mallocque son muy difíciles de predecir.

Primero, dependiendo de cómo esté midiendo la memoria, es posible que solo esté midiendo páginas realmente asignadas a la memoria. En ese caso, cada vez que el localizador cambia una página, la memoria aparecerá como "liberada", aunque no se haya liberado.

O puede estar midiendo páginas en uso, que pueden o no contar páginas asignadas pero nunca tocadas (en sistemas que se sobreasignan de manera optimista, como Linux), páginas que están asignadas pero etiquetadas MADV_FREE , etc.

Si realmente está midiendo las páginas asignadas (que en realidad no es algo muy útil, pero parece ser lo que está preguntando), y las páginas realmente se han desasignado, dos circunstancias en las que esto puede suceder: ha usado brko equivalente para reducir el segmento de datos (muy raro hoy en día), o ha usado munmapo similar para liberar un segmento mapeado. (En teoría, también hay una variante menor para este último, ya que hay formas de liberar parte de un segmento mapeado, por ejemplo, robarlo MAP_FIXEDpara un MADV_FREEsegmento que inmediatamente desasigne).

Pero la mayoría de los programas no asignan directamente cosas de páginas de memoria; usan un mallocasignador de estilo. Cuando llama free, el asignador solo puede liberar páginas al sistema operativo si resulta serfree el último objeto vivo en una asignación (o en las últimas N páginas del segmento de datos). No hay forma de que su aplicación pueda predecir esto razonablemente, o incluso detectar que sucedió de antemano.

CPython hace que esto sea aún más complicado: tiene un asignador de objetos de 2 niveles personalizado encima de un asignador de memoria personalizado encima malloc. (Consulte los comentarios de origen para obtener una explicación más detallada). Y además de eso, incluso a nivel de API C, mucho menos Python, ni siquiera controla directamente cuándo se desasignan los objetos de nivel superior.

Entonces, cuando liberas un objeto, ¿cómo sabes si va a liberar memoria al sistema operativo? Bueno, primero debe saber que ha publicado la última referencia (incluidas las referencias internas que no conocía), lo que permite que el GC la desasigne. (A diferencia de otras implementaciones, al menos CPython desasignará un objeto tan pronto como se permita). Esto generalmente desasigna al menos dos cosas en el siguiente nivel hacia abajo (por ejemplo, para una cadena, está liberando el PyStringobjeto y el búfer de cadena )

Si lo haces desasignar un objeto, para saber si esto hace que el siguiente nivel hacia abajo para cancelar la asignación de un bloque de almacenamiento de objetos, usted tiene que saber el estado interno del asignador de objeto, así como la forma en que está implementado. (Obviamente, esto no puede suceder a menos que desasigne la última cosa del bloque, e incluso así, puede que no suceda).

Si haces desasignar un bloque de almacenamiento de objetos, para saber si esto provoca una freellamada, usted tiene que saber el estado interno del asignador PyMem, así como la forma en que está implementado. (Nuevamente, debe desasignar el último bloque en uso dentro de una mallocregión ed, e incluso entonces, puede que no suceda).

Si hace free una mallocregión ed, para saber si esto causa un munmapo equivalente (o brk), debe conocer el estado interno del malloc, así como también cómo se implementa. Y este, a diferencia de los otros, es altamente específico de la plataforma. (Y, de nuevo, generalmente debe desasignar el último uso mallocdentro de unmmap segmento, e incluso entonces, puede que no suceda)

Entonces, si quieres entender por qué se lanzó exactamente 50.5mb, tendrás que rastrearlo de abajo hacia arriba. ¿Por qué mallocanular el mapa de 50.5mb de páginas cuando realizó esas una o más freellamadas (probablemente por un poco más de 50.5mb)? Tendría que leer la plataforma mallocy luego recorrer las diferentes tablas y listas para ver su estado actual. (En algunas plataformas, incluso puede hacer uso de información a nivel del sistema, que es prácticamente imposible de capturar sin hacer una instantánea del sistema para inspeccionar sin conexión, pero afortunadamente esto no suele ser un problema). Y luego tienes que haz lo mismo en los 3 niveles por encima de eso.

Entonces, la única respuesta útil a la pregunta es "Porque".

A menos que esté haciendo un desarrollo de recursos limitados (por ejemplo, incrustado), no tiene motivos para preocuparse por estos detalles.

Y si está haciendo un desarrollo de recursos limitados, conocer estos detalles es inútil; prácticamente tiene que hacer una ejecución final alrededor de todos esos niveles y específicamente mmapla memoria que necesita a nivel de aplicación (posiblemente con un asignador de zona específico de aplicación simple y bien entendido en el medio).

abarnert
fuente
2

Primero, es posible que desee instalar miradas:

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

¡Entonces ejecútalo en la terminal!

glances

En su código de Python, agregue al comienzo del archivo lo siguiente:

import os
import gc # Garbage Collector

Después de usar la variable "Big" (por ejemplo: myBigVar) para la cual desea liberar memoria, escriba en su código de Python lo siguiente:

del myBigVar
gc.collect()

En otra terminal, ejecute su código de Python y observe en la terminal de "miradas", ¡cómo se gestiona la memoria en su sistema!

¡Buena suerte!

PD: supongo que estás trabajando en un sistema Debian o Ubuntu

de20ce
fuente