Liberar memoria en Python

Tengo algunas preguntas relacionadas con el uso de la 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 sube a 80.9mb . Entonces yo,

     del foo 

    La memoria real baja, pero solo a 30.4mb . El intérprete usa 4.4mb línea de base de 4.4mb ¿cuál es la ventaja de no liberar 26mb de memoria al sistema operativo? ¿Es porque Python está “planeando por adelantado”, pensando que puedes usar esa cantidad de memoria nuevamente?

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

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

NOTA Esta pregunta es diferente de ¿Cómo puedo liberar explícitamente la memoria 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 a través de la recolección de basura (con el uso de gc.collect o no).

La memoria asignada en el montón puede estar sujeta a marcas de nivel alto. Esto se complica por las optimizaciones internas de Python para asignar objetos pequeños ( PyObject_Malloc ) en 4 grupos de KiB, clasificados por 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 256 arenas de KiB, por lo que si solo se utiliza un bloque en un grupo, no se lanzará la arena de 256 KiB completa. En Python 3.3, el asignador de objetos pequeños se cambió al uso de mapas de memoria anónimos en lugar del montón, por lo que debería funcionar mejor al liberar memoria.

Además, los tipos incorporados mantienen listas libres de objetos asignados previamente que pueden o no usar el asignador de objetos pequeños. El tipo int mantiene una lista de funciones libre con su propia memoria asignada, y su borrado requiere llamar a PyInt_ClearFreeList() . Esto puede ser llamado indirectamente haciendo un gc.collect completo.

Pruébalo así, y dime lo que 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 la medición en relación con el tamaño de la VM del proceso para eliminar los efectos de otros procesos en el sistema.

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

En el range 3.x no se crea una lista, por lo que la prueba anterior no creará 10 millones de objetos int . Incluso si lo hiciera, el tipo int en 3.x es básicamente un 2.x long , lo que no implementa una lista de confianza.

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

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

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

Si necesita 500MB de almacenamiento temporal por 5 minutos, pero después de eso necesita correr por otras 2 horas y no volverá a tocar tanta memoria, engendre un proceso secundario para hacer el trabajo que requiere mucha memoria. Cuando el proceso hijo 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 para que el comercio valga la pena.

Primero, la forma más fácil de crear un proceso hijo es con concurrent.futures (o, para 3.1 y anteriores, el puerto de futures 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, utilice el módulo de multiprocessing .

Los costos son:

  • El inicio del proceso es un poco lento en algunas plataformas, especialmente en Windows. Aquí estamos hablando en milisegundos, no en minutos, y si está girando un niño para hacer un trabajo de 300 segundos, ni siquiera lo notará. Pero no es gratis.
  • Si la gran cantidad de memoria temporal que utiliza es realmente grande , hacer esto puede hacer que su progtwig principal se cambie. Por supuesto que está ahorrando tiempo a largo plazo, porque si esa memoria se quedara para siempre, tendría que conducir al intercambio en algún momento. Pero esto puede hacer que la lentitud gradual se convierta en demoras notables de una vez (y anticipadas) 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 está enviando y recibiendo grandes cantidades de datos, querrá usar algún otro mecanismo. (un archivo, mmap o de otro tipo; las API de memoria compartida en multiprocessing ; etc.).
  • El envío de grandes cantidades de datos entre procesos significa que los datos deben poder ser recolectados (o, si los guarda en un archivo o en una memoria compartida, se pueden construir con struct o idealmente con ctypes datos).

eryksun ha respondido la pregunta # 1, y yo he respondido la pregunta # 3 (el # 4 original), pero ahora vamos a responder la pregunta # 2:

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

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

Primero, dependiendo de cómo mida la memoria, es posible que solo esté midiendo las páginas asignadas en la memoria. En ese caso, cada vez que una página sea intercambiada por el paginador, la memoria aparecerá como “liberada”, aunque no haya sido liberada.

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

Si realmente está midiendo las páginas asignadas (lo que en realidad no es una cosa muy útil de hacer, pero parece ser lo que está preguntando), y las páginas realmente se han desasignado, existen dos circunstancias en las que esto puede suceder: cualquiera de los dos He usado brk o equivalente para reducir el segmento de datos (muy raro hoy en día), o has usado munmap o similar para liberar un segmento mapeado. (En teoría, también hay una variante menor de esta última, ya que hay formas de liberar parte de un segmento mapeado, por ejemplo, robarlo con MAP_FIXED para un segmento MADV_FREE que inmediatamente se desasigna).

Pero la mayoría de los progtwigs no asignan directamente cosas fuera de las páginas de memoria; utilizan un asignador de estilo malloc . Cuando llama free , el asignador solo puede liberar páginas para el sistema operativo si está liberando 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 lo hace aún más complicado: tiene un asignador de objetos de 2 niveles personalizado encima de un asignador de memoria personalizado encima de malloc . (Consulte los comentarios de la fuente para obtener una explicación más detallada). Y además de eso, incluso en el nivel de API de C, y mucho menos en Python, ni siquiera controla directamente cuándo se desasignan los objetos de nivel superior.

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

Si desasigna un objeto, para saber si esto hace que el siguiente nivel hacia abajo desasigne un bloque de almacenamiento de objetos, debe conocer el estado interno del asignador de objetos, así como la forma en que se implementa. (Obviamente, no puede suceder a menos que esté desasignado lo último en el bloque, e incluso entonces, puede que no suceda).

Si desasigna un bloque de almacenamiento de objetos, para saber si esto causa una llamada free , debe conocer el estado interno del asignador de PyMem, así como la forma en que se implementa. (De nuevo, tiene que desasignar el último bloque en uso dentro de una región malloc , y aún así, puede que no ocurra).

Si free una región munmap , para saber si esto causa un munmap o equivalente (o brk ), debe conocer el estado interno del malloc , así como la forma en que se implementa. Y este, a diferencia de los otros, es altamente específico de la plataforma. (Y de nuevo, generalmente tiene que desasignar el último malloc en uso dentro de un segmento mmap , y aun así, puede que no ocurra).

Por lo tanto, si desea comprender por qué sucedió que se liberaron exactamente 50.5 mb, tendrá que rastrearlo de abajo hacia arriba. ¿Por qué malloc desasignó 50.5mb de páginas cuando realizaste esas una o más llamadas free (probablemente un poco más de 50.5mb)? Tendría que leer el malloc su plataforma y luego recorrer las distintas tablas y listas para ver su estado actual. (En algunas plataformas, incluso puede hacer uso de información a nivel del sistema, que es bastante imposible de capturar sin hacer una instantánea del sistema para inspeccionar fuera de línea, pero afortunadamente esto no suele ser un problema). Y luego tienes que haz lo mismo en los 3 niveles por encima de eso.

Por lo tanto, la única respuesta útil a la pregunta es “Porque”.

A menos que esté realizando un desarrollo con recursos limitados (por ejemplo, integrado), no tiene por qué preocuparse por estos detalles.

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

En primer lugar, 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 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!

PS Supongo que estás trabajando en un sistema Debian o Ubuntu