Entendiendo el uso de la memoria en Python

Estoy tratando de entender cómo Python está usando la memoria para estimar cuántos procesos puedo ejecutar a la vez. En este momento, proceso archivos grandes en un servidor con grandes cantidades de RAM (~ 90-150GB de RAM libre).

Para una prueba, haría cosas en python, luego miraría htop para ver de qué se trataba.

Paso 1: abro un archivo de 2.55GB y lo guardo en una cadena

with open(file,'r') as f: data=f.read() 

El uso es 2686M

Paso 2: Divido el archivo en nuevas líneas.

 data = data.split('\n') 

el uso es 7476M

Paso 3: mantengo solo cada cuarta línea (dos de las tres líneas que quito tienen la misma longitud que la línea que mantengo)

 data=[data[x] for x in range(0,len(data)) if x%4==1] 

el uso es 8543M

paso 4: divido esto en 20 partes iguales para ejecutarlo a través de un grupo de multiprocesamiento.

 l=[] for b in range(0,len(data),len(data)/40): l.append(data[b:b+(len(data)/40)]) 

el uso es 8621M

Paso 5: borro datos, uso es 8496M.

Hay varias cosas que no tienen sentido para mí.

En el paso dos, ¿por qué el uso de la memoria aumenta tanto cuando cambio la cadena en una matriz? ¿Supongo que los contenedores de matriz son mucho más grandes que el contenedor de cadena?

en el paso tres, ¿por qué los datos no se reducen significativamente? Básicamente me deshice de 3/4 de mis arreglos y al menos 2/3 de los datos dentro del arreglo. Espero que se reduzca en consecuencia. Llamar al recolector de basura no hizo ninguna diferencia.

por extraño que parezca, cuando asigné la matriz más pequeña a otra variable, usa menos memoria. uso 6605M

Cuando borro los data objeto antiguo: uso 6059M

Esto me parece raro. Cualquier ayuda para reducir mi huella de memoria sería apreciada.

EDITAR

Está bien, esto está haciendo que me duela la cabeza. Claramente, python está haciendo algunas cosas raras detrás de escena aquí … y solo python. He hecho el siguiente script para demostrarlo usando mi método original y el método sugerido en la respuesta a continuación. Los números están todos en GB.

CÓDIGO DE PRUEBA

 import os,sys import psutil process = psutil.Process(os.getpid()) import time py_usage=process.memory_info().vms / 1000000000.0 in_file = "14982X16.fastq" def totalsize(o): size = 0 for x in o: size += sys.getsizeof(x) size += sys.getsizeof(o) return "Object size:"+str(size/1000000000.0) def getlines4(f): for i, line in enumerate(f): if i % 4 == 1: yield line.rstrip() def method1(): start=time.time() with open(in_file,'rb') as f: data = f.read().split("\n") data=[data[x] for x in xrange(0,len(data)) if x%4==1] return data def method2(): start=time.time() with open(in_file,'rb') as f: data2=list(getlines4(f)) return data2 print "method1 == method2",method1()==method2() print "Nothing in memory" print "Usage:", (process.memory_info().vms / 1000000000.0) - py_usage data=method1() print "data from method1 is in memory" print "method1", totalsize(data) print "Usage:", (process.memory_info().vms / 1000000000.0) - py_usage del data print "Nothing in memory" print "Usage:", (process.memory_info().vms / 1000000000.0) - py_usage data2=method2() print "data from method2 is in memory" print "method2", totalsize(data2) print "Usage:", (process.memory_info().vms / 1000000000.0) - py_usage del data2 print "Nothing is in memory" print "Usage:", (process.memory_info().vms / 1000000000.0) - py_usage print "\nPrepare to have your mind blown even more!" data=method1() print "Data from method1 is in memory" print "Usage:", (process.memory_info().vms / 1000000000.0) - py_usage data2=method2() print "Data from method1 and method 2 are in memory" print "Usage:", (process.memory_info().vms / 1000000000.0) - py_usage data==data2 print "Compared the two lists" print "Usage:", (process.memory_info().vms / 1000000000.0) - py_usage del data print "Data from method2 is in memory" print "Usage:", (process.memory_info().vms / 1000000000.0) - py_usage del data2 print "Nothing is in memory" print "Usage:", (process.memory_info().vms / 1000000000.0) - py_usage 

SALIDA

 method1 == method2 True Nothing in memory Usage: 0.001798144 data from method1 is in memory method1 Object size:1.52604683 Usage: 4.552925184 Nothing in memory Usage: 0.001798144 data from method2 is in memory method2 Object size:1.534815518 Usage: 1.56932096 Nothing is in memory Usage: 0.001798144 Prepare to have your mind blown even more! Data from method1 is in memory Usage: 4.552925184 Data from method1 and method 2 are in memory Usage: 4.692287488 Compared the two lists Usage: 4.692287488 Data from method2 is in memory Usage: 4.56169472 Nothing is in memory Usage: 0.001798144 

para aquellos de ustedes que usan python3 es bastante similar, excepto que no es tan malo después de la operación de comparación …

SALIDA DE PYTHON3

 method1 == method2 True Nothing in memory Usage: 0.004395008000000006 data from method1 is in memory method1 Object size:1.718523294 Usage: 5.322555392 Nothing in memory Usage: 0.004395008000000006 data from method2 is in memory method2 Object size:1.727291982 Usage: 1.872596992 Nothing is in memory Usage: 0.004395008000000006 Prepare to have your mind blown even more! Data from method1 is in memory Usage: 5.322555392 Data from method1 and method 2 are in memory Usage: 5.461917696 Compared the two lists Usage: 5.461917696 Data from method2 is in memory Usage: 2.747633664 Nothing is in memory Usage: 0.004395008000000006 

La moraleja de la historia … la memoria de python parece ser un poco como Camelot para Monty Python … ‘es un lugar muy tonto.

Voy a sugerirle que retroceda y se acerque a esto en lugar de una manera que aborde directamente su objective: reducir el uso máximo de memoria para comenzar. Ninguna cantidad de análisis y manipulación posterior puede superarse utilizando un enfoque condenado para comenzar con 😉

Concretamente, comenzó mal con el pie en el primer paso, a través de data=f.read() . Ahora ya es posible que su progtwig no pueda escalar más allá de un archivo de datos que se ajuste completamente en la RAM con espacio de sobra (para ejecutar el sistema operativo y Python y …) también.

¿Realmente necesitas que todos los datos estén en la memoria RAM al mismo tiempo? Hay muy pocos detalles para contar acerca de los pasos posteriores, pero obviamente no al principio, ya que inmediatamente desea eliminar el 75% de las líneas que lee.

Así que empieza haciendo eso incrementalmente en su lugar:

 def getlines4(f): for i, line in enumerate(f): if i % 4 == 1: yield line 

Incluso si no hace nada más que eso, puede saltar directamente al resultado del paso 3, ahorrando una enorme cantidad de uso máximo de RAM:

 with open(file, 'r') as f: data = list(getlines4(f)) 

Ahora, la necesidad máxima de RAM es proporcional al número de bytes en las únicas líneas que le interesan, en lugar del número total de bytes del archivo.

Para continuar progresando, en lugar de materializar todas las líneas de interés en los data de un gulp gigante, alimente las líneas (o trozos de líneas) de manera incremental a sus procesos de trabajo también. No tuve suficientes detalles para sugerir un código concreto para eso, pero tenga en cuenta el objective y lo resolverá: solo necesita suficiente RAM para seguir alimentando de forma gradual las líneas a los procesos de trabajo, y ahorrar, por mucho que sea. Los resultados de los procesos de trabajo que necesita mantener en la memoria RAM. Es posible que el uso máximo de la memoria no necesite más que “pequeño”, independientemente del tamaño del archivo de entrada.

Combatir los detalles de la administración de la memoria es enormemente más difícil que comenzar con un enfoque amigable con la memoria. Python tiene varios subsistemas de administración de memoria, y se puede decir mucho sobre cada uno de ellos. A su vez, dependen de la plataforma C malloc / instalaciones gratuitas, sobre las cuales también hay mucho que aprender. Y todavía no estamos en un nivel que tenga nada que ver directamente con lo que su sistema operativo informa sobre el “uso de memoria”. Las bibliotecas de la plataforma C, a su vez, dependen de las primitivas de gestión de la memoria del sistema operativo específico de la plataforma, que, por lo general, solo los expertos en memoria del kernel del sistema operativo comprenden realmente.

La respuesta a “¿por qué el sistema operativo dice que todavía estoy usando N GiB de RAM?” puede confiar en detalles específicos de la aplicación en cualquiera de esas capas, o incluso en desafortunadas interacciones más o menos accidentales entre ellas. Es mucho mejor hacer arreglos para no tener que hacer esas preguntas para empezar.

EDITAR – sobre obmalloc de CPython

Es genial que hayas dado un código ejecutable, pero no tanto para que nadie pueda ejecutarlo, ya que nadie más tiene tus datos 😉 Cosas como “¿cuántas líneas hay?” y “¿cuál es la distribución de longitudes de línea?” Puede ser crítico, pero no tenemos forma de adivinar.

Como señalé anteriormente, los detalles específicos de la aplicación a menudo son necesarios para superar a los administradores de memoria modernos. Son complejos, y el comportamiento en todos los niveles puede ser sutil.

El asignador principal de objetos de Python (“obmalloc”) solicita “arenas” de la plataforma C malloc, trozos de 2 ** 18 bytes. Siempre que ese sea el sistema de memoria Python que está utilizando su aplicación (lo cual no se puede adivinar porque no tenemos sus datos con los que trabajar), 256 KiB es la granularidad más pequeña a la que se solicita o se devuelve a la memoria. El nivel C El nivel de C, a su vez, por lo general tiene estrategias de “fragmentación”, que varían según las implementaciones de C.

A su vez, una arena de Python está tallada en 4 “grupos” de KiB, cada uno de los cuales se adapta dinámicamente para ser tallado en trozos más pequeños de un tamaño fijo por grupo (fragmentos de 8 bytes, fragmentos de 16 bytes, fragmentos de 24 bytes, … , 8 * trozos de i-byte por grupo).

Mientras se use un solo byte en una arena para datos en vivo, se debe conservar la arena completa . Si eso significa que los otros 262,143 bytes de arena quedan sin uso, mala suerte. Como muestra su salida, toda la memoria se devuelve al final, así que, ¿por qué le importa realmente? Entiendo que es un rompecabezas abstracto interesante, pero no va a resolverlo, obmalloc.c de hacer grandes esfuerzos para entender el código en obmalloc.c de CPython. Para comenzar. Cualquier “resumen” dejaría de lado un detalle que es realmente importante para el comportamiento microscópico de alguna aplicación.

Plausible: sus cadenas son lo suficientemente cortas para que el espacio para todos los encabezados y contenidos de los objetos de la cadena (los datos reales de la cadena) se obtengan del obmalloc de CPython. Van a ser salpicados en múltiples arenas. Una arena podría tener este aspecto, donde “H” representa los grupos a partir de los cuales se asignan los encabezados de los objetos de cadena y los grupos “D” a los que se asigna el espacio para los datos de cadena:

 HHDDHHDDHHDDHHDDHHDDHHDDHHDDHHDDHHDDHHDDHHDDHHDDHHDDHHDD... 

En su method1 , tenderán a alternar “así” porque la creación de un único objeto de cadena requiere la asignación de espacio por separado para el encabezado del objeto de cadena y los datos del objeto de cadena. Cuando vas a tirar 3/4 de las cadenas que creaste, más o menos 3/4 de ese espacio se vuelven reutilizables para Python . Pero no se puede devolver un byte al sistema C porque aún hay datos en vivo distribuidos por toda la arena, que contienen la cuarta parte de los objetos de cadena que no tiró (aquí “-” significa espacio disponible para su reutilización):

 HHDD------------HHDD------------HHDD------------HHDD----... 

Hay tanto espacio libre que, de hecho, es posible que el method2 menos desperdiciado2 pueda obtener toda la memoria que necesita de los orificios -------- quedan del method1 incluso cuando no tira el resultado del method1 .

Solo para mantener las cosas simples ;-), señalaré que algunos de esos detalles sobre cómo se utiliza el obmalloc de CPython también varían en las versiones de Python. En general, cuanto más reciente es la versión de Python, más intenta utilizar obmalloc primero en lugar de la plataforma C malloc / free (porque obmalloc es generalmente más rápido).

Pero incluso si usa la plataforma C malloc / free directamente, puede ver el mismo tipo de cosas sucediendo. Las llamadas al sistema de memoria del kernel suelen ser más caras que la ejecución de código únicamente en el espacio del usuario, por lo que las rutinas de malloc / plataforma de la plataforma C suelen tener sus propias estrategias para “pedir al kernel mucha más memoria de la que necesitamos para una sola solicitud, y dividirla piezas más pequeñas nosotros mismos “.

Algo a tener en cuenta: ni el obmalloc de Python ni las implementaciones gratuitas de mallorm de Cormorm C mueven los datos en vivo por su cuenta. Ambos devuelven las direcciones de memoria a los clientes, y esas no pueden cambiar. Los “agujeros” son un hecho ineludible de la vida en ambos.