Liberando memoria en Python
Tengo algunas preguntas relacionadas con el uso de la memoria en el siguiente ejemplo.
Si ejecuto el intérprete,
foo = ['bar' for _ in xrange(10000000)]
la memoria real utilizada en mi máquina llega a
80.9mb
. Entonces yo,del foo
La memoria real baja, pero sólo hasta
30.4mb
. El intérprete utiliza4.4mb
la línea de base, entonces, ¿cuál es la ventaja de no liberar26mb
memoria al sistema operativo? ¿Es porque Python está "planificando con anticipación" y piensa que es posible que vuelva a utilizar tanta memoria?¿Por qué se libera
50.5mb
en particular? ¿En qué se basa la cantidad que se libera?¿Hay alguna manera 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 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 de gc.collect
o no).
Supongo que la pregunta que realmente te interesa aquí es:
¿Hay alguna manera 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 existe una solución sencilla: los procesos secundarios.
Si necesita 500 MB de almacenamiento temporal durante 5 minutos, pero después de eso necesita ejecutar durante otras 2 horas y no volverá a tocar tanta memoria, genere un proceso hijo para realizar el trabajo que requiere mucha memoria. Cuando el proceso hijo desaparece, la memoria se libera.
Esto no es completamente trivial ni gratuito, pero es bastante fácil y barato, lo que suele ser lo suficientemente bueno como para que el intercambio valga la pena.
Primero, la forma más sencilla de crear un proceso hijo es con concurrent.futures
(o, para 3.1 y versiones anteriores, el futures
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, utilice el multiprocessing
módulo.
Los costos son:
- El inicio del proceso es algo lento en algunas plataformas, especialmente en Windows. Estamos hablando de milisegundos, no de minutos, y si estás haciendo girar a un niño para que haga 300 segundos de trabajo, ni siquiera lo notarás. Pero no es gratis.
- Si la gran cantidad de memoria temporal que utiliza realmente es grande , hacer esto puede provocar que se intercambie su programa principal. Por supuesto, estás ahorrando tiempo a largo plazo, porque si esa memoria permaneciera para siempre, tendría que dar lugar a un intercambio en algún momento. Pero esto puede convertir la lentitud gradual en retrasos muy notables (y tempranos) en algunos casos de uso.
- El envío de grandes cantidades de datos entre procesos puede resultar lento. Nuevamente, si está hablando de enviar más de 2 000 argumentos y obtener 64 000 resultados, ni siquiera lo notará, pero si envía y recibe grandes cantidades de datos, querrá utilizar algún otro mecanismo. (un archivo,
mmap
ped o de otro tipo; las API de memoria compartida enmultiprocessing
; etc.). - Enviar grandes cantidades de datos entre procesos significa que los datos deben ser seleccionables (o, si los guarda en un archivo o memoria compartida,
struct
-capaces o idealmentectypes
-capaces).
La memoria asignada en el montón puede estar sujeta a marcas de límite máximo. Esto se complica por las optimizaciones internas de Python para asignar objetos pequeños ( PyObject_Malloc
) en grupos de 4 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á toda la arena de 256 KiB. En Python 3.3, el asignador de objetos pequeños se cambió para usar mapas de memoria anónimos en lugar del montón, por lo que debería funcionar mejor en la liberación de memoria.
Además, los tipos integrados mantienen listas libres de objetos previamente asignados que pueden o no utilizar el asignador de objetos pequeños. El int
tipo mantiene una lista libre con su propia memoria asignada y para borrarla es necesario llamar a PyInt_ClearFreeList()
. Esto se puede llamar indirectamente haciendo un full 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.memory_info().rss
# create approx. 10**7 int objects and pointers
foo = ['abc' for x in range(10**7)]
mem1 = proc.memory_info().rss
# unreference, including x == 9999999
del foo, x
mem2 = proc.memory_info().rss
# collect() calls PyInt_ClearFreeList()
# or use ctypes: pythonapi.PyInt_ClearFreeList()
gc.collect()
mem3 = proc.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)
Producción:
Allocation: 3034.36%
Unreference: -752.39%
Collect: -2279.74%
Overall: 2.23%
Editar:
Cambié a medir en relación con el tamaño de la máquina virtual del proceso para eliminar los efectos de otros procesos en el sistema.
El tiempo de ejecución de 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 puedes 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 range
no se crea una lista, por lo que la prueba anterior no creará 10 millones int
de objetos. Incluso si así fuera, el int
tipo en 3.x es básicamente 2.x long
, que no implementa una lista libre.
eryksun respondió la pregunta n.° 1 y yo respondí la pregunta n.° 3 (la n.° 4 original), pero ahora respondamos la pregunta n.° 2:
¿Por qué libera 50,5 MB en particular? ¿En qué se basa la cantidad que se libera?
En lo que se basa, en definitiva, es en 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 páginas realmente asignadas a la memoria. En ese caso, cada vez que el buscapersonas intercambia una página, la memoria aparecerá como "liberada", aunque no se haya liberado.
O puede estar midiendo las páginas en uso, que pueden contar o no las páginas asignadas pero nunca tocadas (en sistemas que sobreasignan de manera optimista, como Linux), páginas asignadas pero etiquetadas MADV_FREE
, etc.
Si realmente está midiendo las páginas asignadas (lo cual en realidad no es algo muy útil, pero parece ser lo que está preguntando) y las páginas realmente han sido desasignadas, hay dos circunstancias en las que esto puede suceder: Ha usado brk
o equivalente para reducir el segmento de datos (muy raro hoy en día), o ha usado munmap
o similar para liberar un segmento mapeado. (En teoría, también existe una variante menor de esto último, en el sentido de que hay formas de liberar parte de un segmento mapeado; por ejemplo, robarlo MAP_FIXED
para un MADV_FREE
segmento que se desasigna inmediatamente).
Pero la mayoría de los programas no asignan directamente cosas fuera de las páginas de memoria; utilizan un malloc
asignador de estilo. Cuando llama free
, el asignador solo puede liberar páginas al sistema operativo si se encuentra free
en el último objeto activo en un mapeo (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ó con anticipación.
CPython hace que esto sea aún más complicado: tiene un asignador de objetos personalizado de 2 niveles además 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 liberas un objeto, ¿cómo sabes si 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 permite al GC desasignarla. (A diferencia de otras implementaciones, al menos CPython desasignará un objeto tan pronto como se le permita). Esto generalmente desasigna al menos dos cosas en el siguiente nivel inferior (por ejemplo, para una cadena, estás liberando el PyString
objeto y el búfer de cadena). ).
Si desasigna un objeto, para saber si esto hace que el siguiente nivel desasigne un bloque de almacenamiento de objetos, debe conocer el estado interno del asignador de objetos, así como también cómo se implementa. (Obviamente, no puede suceder a menos que desasignes lo último del bloque, e incluso entonces, puede que no suceda).
Si desasignas un bloque de almacenamiento de objetos, para saber si esto provoca una free
llamada, debes conocer el estado interno del asignador de PyMem, así como también cómo se implementa. (Nuevamente, debe desasignar el último bloque en uso dentro de una malloc
región educativa, e incluso entonces, es posible que no suceda).
Si crea free
una malloc
región ed, para saber si esto causa un munmap
equivalente (o brk
), debe conocer el estado interno de malloc
, así como también cómo se implementa. Y éste, a diferencia de los demás, es muy específico de la plataforma. (Y nuevamente, generalmente hay que desasignar el último en uso malloc
dentro de un mmap
segmento, e incluso entonces, puede que no suceda).
Entonces, si quieres entender por qué se liberó exactamente 50,5 MB, tendrás que rastrearlo de abajo hacia arriba. ¿ Por qué malloc
desasignó 50,5 MB de páginas cuando realizó esas una o más free
llamadas (probablemente por un poco más de 50,5 MB)? Tendría que leer el contenido de su plataforma malloc
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, lo cual es prácticamente imposible de capturar sin tomar una instantánea del sistema para inspeccionarlo fuera de línea, pero afortunadamente esto no suele ser un problema). Y luego hay que hacerlo. Haz lo mismo en los 3 niveles superiores.
Entonces, 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á realizando un desarrollo con recursos limitados, conocer estos detalles es inútil; prácticamente tienes que hacer una ejecución final en todos esos niveles y específicamente en mmap
la memoria que necesitas en el nivel de la aplicación (posiblemente con un asignador de zona simple, bien entendido y específico de la aplicación en el medio).