objetos objeto memoria manejo maneja limpiar liberar eliminar destruir como python memory-management

objeto - limpiar variables en python



Liberando memoria en Python (3)

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 disminuye, pero solo a 30.4mb . El intérprete utiliza 4.4mb línea base de 4.4mb entonces, ¿cuál es la ventaja de no liberar 26mb de memoria para el sistema operativo? ¿Es porque Python está "planeando por adelantado", pensando que puede usar esa cantidad de memoria otra vez?

  2. ¿Por qué libera 50.5mb en particular? ¿ 50.5mb es la cantidad que se libera según?

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


La memoria asignada en el montón puede estar sujeta a marcas de agua alta. Esto se complica con las optimizaciones internas de Python para asignar objetos pequeños ( PyObject_Malloc ) en 4 grupos KiB, clasificados para tamaños de asignación en múltiplos de 8 bytes: hasta 256 bytes (512 bytes en 3.3). Las piscinas en sí mismas están en 256 arenas KiB, por lo que si se usa solo un bloque en un grupo, no se lanzará todo el campo 256 KiB. En Python 3.3, el pequeño asignador de objetos se cambió a 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 incorporados mantienen listas de objetos previamente asignados que pueden o no pueden usar el asignador de objetos pequeños. El tipo int mantiene una lista libre con su propia memoria asignada, y PyInt_ClearFreeList() requiere llamar a PyInt_ClearFreeList() . Esto se puede llamar indirectamente haciendo un gc.collect completo.

Pruébalo así y cuéntame lo que obtienes. Aquí está el enlace para psutil .

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 relativa al tamaño de VM de 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 sintonizar esto con mallopt (M_TRIM_THRESHOLD). Dado esto, no es sorprendente si el montón se reduce más, incluso 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 de long , que no implementa un libreto.


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

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

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 funcionar durante otras 2 horas y no volverá a tocar esa cantidad de memoria, genere un proceso secundario para realizar un trabajo que requiere mucho tiempo de memoria. Cuando el proceso del niño desaparece, la memoria se libera.

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

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

Los costos son:

  • El inicio del proceso es algo lento en algunas plataformas, especialmente en Windows. Estamos hablando de milisegundos aquí, no de minutos, y si está haciendo girar a un niño para que haga 300 segundos de trabajo, ni siquiera lo notará. Pero no es gratis.
  • Si la gran cantidad de memoria temporal que utiliza realmente es grande , hacer esto puede hacer que su programa principal se cancele. Por supuesto que está ahorrando tiempo a largo plazo, porque si ese recuerdo permaneciera para siempre tendría que conducir al intercambio en algún momento. Pero esto puede convertir la lentitud gradual en retrasos muy notables todo en uno (y temprano) 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 obtener 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 ped u otro, las API de memoria compartida en multiprocessing , etc.).
  • Enviar grandes cantidades de datos entre procesos significa que los datos tienen que ser seleccionables (o, si los incluyes en un archivo o memoria compartida, struct o idealmente ctypes -able).

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

¿Por qué libera 50.5mb en particular? ¿Cuál es la cantidad que se libera según?

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.

En primer lugar, dependiendo de cómo esté midiendo la memoria, es posible que solo esté midiendo las páginas realmente mapeadas en la memoria. En ese caso, cada vez que el buscapersonas canjea una página, la memoria aparecerá como "liberada", aunque no se haya liberado.

O puede estar midiendo páginas en uso, que pueden contar o no las páginas asignadas pero nunca tocadas (en sistemas que MADV_FREE 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 que hacer, pero parece ser lo que está preguntando), y las páginas realmente han sido desasignadas, dos circunstancias en las que esto puede suceder: o '' he 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. (También hay una variante teóricamente menor a la última, en el sentido de que hay formas de liberar parte de un segmento mapeado; por ejemplo, robarlo con MAP_FIXED para un segmento MADV_FREE que inmediatamente desasignar).

Pero la mayoría de los programas no asignan directamente las páginas de memoria; usan un asignador de estilo malloc . Cuando llame free , el asignador solo puede liberar páginas en el sistema operativo si acaba de free el último objeto activo 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 razonablemente esto, 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 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 va a liberar memoria en el sistema operativo? Bueno, primero debe saber que ha liberado 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 le sea permitido). Esto suele desasignar al menos dos cosas en el siguiente nivel (por ejemplo, para una cadena, está liberando el objeto PyString y la cadena buffer).

Si desasigna un objeto, para saber si esto causa 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 esté desasignando lo último en el bloque, e incluso entonces, puede no suceder).

Si desasigna un bloque de almacenamiento de objetos, para saber si esto provoca una llamada free , debe conocer el estado interno del asignador PyMem, así como también cómo se implementa. (Una vez más, debe desasignar el último bloque en uso dentro de una región malloc e incluso entonces puede no suceder).

Si free una región malloc ed, para saber si esto causa un munmap o equivalente (o brk ), debe conocer el estado interno del malloc , así como también cómo se implementa. Y este, a diferencia de los demás, es altamente específico de la plataforma. (Y nuevamente, generalmente tiene que desasignar el último malloc en uso dentro de un segmento de mmap , e incluso entonces, puede que no ocurra).

Por lo tanto, si quieres entender por qué sucedió lanzar exactamente 50.5mb, vas a tener que rastrearlo de abajo hacia arriba. ¿Por qué Malloc desasignó 50.5mb de páginas cuando hizo esas llamadas free (probablemente por poco más de 50.5mb)? Tendría que leer el malloc su plataforma y luego recorrer las diversas tablas y listas para ver su estado actual. (En algunas plataformas, incluso puede hacer uso de la información del nivel del sistema, que es prácticamente 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.

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

A menos que tenga un desarrollo de recursos limitados (por ejemplo, integrado), no tiene motivos para preocuparse por estos detalles.

Y si está haciendo un desarrollo de recursos limitados, conocer estos detalles es inútil; más o menos tiene que hacer una evaluación final en todos los niveles y específicamente mmap la memoria que necesita en el nivel de aplicación (posiblemente con un asignador de zona simple, bien entendido y específico de la aplicación).