Python Memory Management: un poco sobre la fragmentación de la memoria

Algunas reflexiones sobre este artículo .

Recientemente me interesé en cómo funciona Python Memory Management en CPython para Python3 para Ubuntu de 64 bits .

Poco de teoría


La biblioteca del sistema glibc tiene un asignador malloc. Cada proceso tiene un área de memoria llamada montón. Al asignar memoria dinámicamente llamando a la función malloc, obtenemos una porción del montón de este proceso. Si el tamaño de la memoria solicitada es pequeño (no más de 128 KB), la memoria se puede tomar de las listas de fragmentos libres. Si esto no es posible, la memoria se asignará mediante la llamada al sistema mmap (sbrk, brk) . La llamada al sistema mmap asigna memoria virtual a memoria física. La memoria se muestra en páginas de 4KB. Los fragmentos grandes (más de 128 KB) siempre se asignan a través de la llamada al sistema mmap. Al liberar memoria, si un pequeño fragmento libre bordea un área de memoria no congelada, parte de la memoria puede volver al sistema operativo. Los fragmentos grandes vuelven inmediatamente al sistema operativo.

Información tomada de una conferencia sobre asignadores en C.

CPython tiene su propio asignador (PyMalloc) para el "montón privado" y asignadores para cada tipo de objeto que funciona "encima" del primero. PyMalloc solicita fragmentos de memoria de 256 KB a través de malloc en la biblioteca del sistema operativo llamada Arenas. Ellos, a su vez, se dividen en Pools por 4KB. Cada grupo se divide en fragmentos de un tamaño fijo, y cada uno se puede dividir en fragmentos de uno de los 64 tamaños.

Los asignadores para cada tipo utilizan los fragmentos ya asignados, si los hay. Si no hay ninguno, PyMalloc emitirá un nuevo Pool desde la primera Arena, en la que hay un lugar para un nuevo Pool (las Arenas están "ordenadas" en orden descendente de ocupación). Si esto no funciona, PyMalloc le pide al sistema operativo una nueva Arena. Excepto cuando el tamaño de memoria solicitado es superior a 512 B, la memoria se asigna directamente a través de malloc desde la biblioteca del sistema.

Cuando se elimina un objeto, la memoria no se devuelve al sistema operativo, pero los fragmentos simplemente regresan a los grupos correspondientes y los grupos a las arenas. La arena vuelve al sistema operativo cuando se liberan todos los fragmentos. Resulta que si un número relativamente pequeño de Chunks se usará en la Arena, PVM utilizará toda la memoria en la Arena. Pero como los trozos de más de 128 KB se asignan a través de mmap, el Arena gratuito volverá inmediatamente al sistema operativo.

Me gustaría centrarme en dos puntos:

  1. Resulta que PyMalloc asigna 256 KB de memoria física al crear una nueva Arena.
  2. Solo las Arenas gratuitas se devuelven al sistema operativo.

Ejemplo


Considere el siguiente ejemplo:

iterations = 2000000 l = [] for i in range(iterations): l.append(None) for i in range(iterations): l[i] = {} s = [] # [1] # s = l[::2] # [2] # s = l[2000000 // 2::] # [3] # s = l[::100] # [4] for i in range(iterations): l[i] = None for i in range(iterations): l[i] = {} 

En el ejemplo, se crea una lista l de 2 millones de elementos, todos los cuales apuntan a un objeto Ninguno. En el siguiente ciclo, se crea un objeto para cada elemento: un diccionario vacío. Luego se crea una segunda lista s, cuyos elementos apuntan a algunos objetos a los que hacen referencia algunos elementos de la primera lista. Después del siguiente rastreo, los elementos de la lista l nuevamente comienzan a apuntar al objeto Ninguno. Y en el último ciclo, los diccionarios se crean nuevamente para cada elemento de la primera lista.

Opciones de la lista S:

  1.  s = [] 
  2.  s = l[::2] 
  3.  s = l[200000 // 2::] 
  4.  s = l[::100] 

Estamos interesados ​​en el consumo de memoria en cada caso.

Ejecutaremos este script con el registro de PyMalloc habilitado:

 export PYTHONMALLOCSTATS="True" && python3 source.py 2>result.txt 

Explicación de los resultados.


En las imágenes puedes ver el consumo de memoria en cada caso. En el eje de abscisas no hay correlación de valores con el momento en que se produjo dicho consumo, solo cada valor en los registros está asociado con su número de serie.

imagen

"Sin elementos"


En el primer caso, la lista s está vacía. Después de crear los objetos en el segundo ciclo, se consumen aproximadamente 500 MB de memoria. Y todos estos objetos se eliminan en el tercer ciclo, y la memoria utilizada se devuelve al sistema operativo. En el último ciclo, la memoria para objetos se asigna nuevamente, lo que conduce al consumo de los mismos 500 MB.

"Cada segundo"


En el caso en que creamos una lista con cada segundo elemento de la lista l, podemos notar que la memoria no regresa al sistema operativo. Es decir, en este caso observamos una situación en la que se eliminan diccionarios de aproximadamente 250 MB, pero en cada grupo hay fragmentos que no se eliminan, por lo que no se liberan los Arenas correspondientes. Pero, cuando creamos los diccionarios por segunda vez, los trozos libres de estos grupos se reutilizan, por lo que solo se asignan unos 250 MB de memoria nueva.

"La segunda mitad"


En el caso de que creamos una lista a partir de la segunda mitad de los elementos de la lista l, la primera mitad está en Arenas separadas, debido a que aproximadamente 250 MB de memoria se devuelven al sistema operativo. Después de eso, aproximadamente 500 MB se reasignan a nuevos diccionarios, por lo que el consumo total en la región de 750 MB.

En este caso, a diferencia del segundo, la memoria se devuelve parcialmente al sistema operativo. Lo que, por un lado, permite que otros procesos usen esta memoria, por otro lado, requiere llamadas del sistema para liberarla y reasignarla.

"Cada centésima"


El último caso parece ser el más interesante. Allí creamos una segunda lista de cada centésimo elemento de la primera lista, que requiere aproximadamente 5 MB. Pero debido al hecho de que un cierto número de Chunks ocupados permanece en cada Arena, esta memoria no se libera y el consumo permanece en el nivel de 500MB. Cuando creamos diccionarios por segunda vez, casi no se asigna memoria nueva, y los Chunks que se asignaron por primera vez se reutilizan.

En esta situación, debido a la fragmentación de la memoria, utilizamos 100 veces más de lo que necesitamos. Pero, cuando se requiere esta memoria repetidamente, no tenemos que hacer llamadas al sistema para asignarla.

Resumen


Vale la pena señalar que la fragmentación de la memoria es posible cuando se utilizan muchos asignadores. Sin embargo, debe utilizar cuidadosamente algunas estructuras de datos, por ejemplo, aquellas que tienen una estructura de árbol, como los árboles de búsqueda. Debido a que las operaciones arbitrarias de agregar y eliminar pueden conducir a la situación descrita anteriormente, por lo que la conveniencia de usar estas estructuras en términos de consumo de memoria estará en duda.

Código para renderizar imágenes
 def parse_result(filename): ms = [] with open(filename, "r") as f: for line in f: if line.startswith("Total"): m = float(line.split()[-1].replace(",", "")) / 1024 / 1024 ms.append(m) return ms ms_1 = parse_result("_1.txt") ms_2 = parse_result("_2.txt") ms_3 = parse_result("_3.txt") ms_4 = parse_result("_4.txt") import matplotlib.pyplot as plt plt.figure(figsize=(20, 15)) fontdict = { "fontsize": 20, "fontweight" : 1, } plt.subplot(2, 2, 1) plt.title(" ", fontdict=fontdict, loc="left") plt.plot(ms_1) plt.grid(b=True, which='major', color='#666666', linestyle='-.') plt.minorticks_on() plt.grid(b=True, which='minor', color='#999999', linestyle='-', alpha=0.2) plt.tick_params(axis='both', which='major', labelsize=15, labelbottom=False) plt.ylabel("MB", fontsize=15) plt.subplot(2, 2, 2) plt.title(" ", fontdict=fontdict, loc="left") plt.plot(ms_2) plt.grid(b=True, which='major', color='#666666', linestyle='-.') plt.minorticks_on() plt.grid(b=True, which='minor', color='#999999', linestyle='-', alpha=0.2) plt.tick_params(axis='both', which='major', labelsize=15, labelbottom=False) plt.ylabel("MB", fontsize=15) plt.subplot(2, 2, 3) plt.title(" ", fontdict=fontdict, loc="left") plt.plot(ms_3) plt.grid(b=True, which='major', color='#666666', linestyle='-.') plt.minorticks_on() plt.grid(b=True, which='minor', color='#999999', linestyle='-', alpha=0.2) plt.tick_params(axis='both', which='major', labelsize=15, labelbottom=False) plt.ylabel("MB", fontsize=15) plt.subplot(2, 2, 4) plt.title(" ", fontdict=fontdict, loc="left") plt.plot(ms_4) plt.grid(b=True, which='major', color='#666666', linestyle='-.') plt.minorticks_on() plt.grid(b=True, which='minor', color='#999999', linestyle='-', alpha=0.2) plt.tick_params(axis='both', which='major', labelsize=15, labelbottom=False) plt.ylabel("MB", fontsize=15) 

Source: https://habr.com/ru/post/479744/


All Articles