Ir mecanismos de asignación

Cuando intenté entender por primera vez cómo funcionan las herramientas de asignación de memoria en Go, lo que quería tratar parecía una misteriosa caja negra. Al igual que con cualquier otra tecnología, lo más importante aquí está oculto detrás de muchas capas de abstracciones, a través de las cuales debe pasar para comprender algo.



El autor del material, cuya traducción estamos publicando, decidió llegar al fondo de los medios de asignación de memoria en Go y hablar sobre él.

Memoria física y virtual


Todos los medios para asignar memoria tienen que funcionar con el espacio de direcciones de la memoria virtual, que es controlado por el sistema operativo. Veamos cómo funciona la memoria, comenzando en el nivel más bajo, con celdas de memoria.
Aquí se explica cómo imaginar una celda RAM.


Dise√Īo de celda de memoria

Si, muy simplificado, imagina una celda de memoria y lo que la rodea, entonces obtenemos lo siguiente:

  1. La l√≠nea de direcci√≥n (el transistor act√ļa como un interruptor) es lo que da acceso al condensador (l√≠nea de datos).
  2. Cuando aparece una se√Īal (l√≠nea roja) en la l√≠nea de direcci√≥n, la l√≠nea de datos le permite escribir datos en la celda de memoria, es decir, cargar el condensador, lo que permite almacenar un valor l√≥gico correspondiente a 1 en √©l.
  3. Cuando no hay se√Īal en la l√≠nea de direcci√≥n (l√≠nea verde), el capacitor est√° aislado y su carga no cambia. Para escribir en la celda 0, debe seleccionar su direcci√≥n y enviar un 0 l√≥gico a trav√©s de la l√≠nea de datos, es decir, conectar la l√≠nea de datos con un signo menos, descargando as√≠ el condensador.
  4. Cuando el procesador necesita leer el valor de la memoria, la se√Īal se env√≠a a lo largo de la l√≠nea de direcci√≥n (el interruptor se cierra). Si el condensador est√° cargado, la se√Īal pasa por la l√≠nea de datos (se lee 1), de lo contrario, la se√Īal no pasa por la l√≠nea de datos (se lee 0).


El esquema de interacción de la memoria física y el procesador.

El bus de datos es responsable de transportar los datos entre el procesador y la memoria física.

Ahora hablemos de la línea de dirección y los bytes direccionables.


Líneas de dirección de bus entre procesador y memoria física

  1. A cada byte en RAM se le asigna un identificador num√©rico √ļnico (direcci√≥n). Cabe se√Īalar que el n√ļmero de bytes f√≠sicos presentes en la memoria no es igual al n√ļmero de l√≠neas de direcci√≥n.
  2. Cada línea de dirección puede especificar un valor de 1 bit, por lo que indica un bit en la dirección de un determinado byte.
  3. Nuestro circuito tiene 32 l√≠neas de direcci√≥n. Como resultado, cada byte direccionable utiliza un n√ļmero de 32 bits como su direcci√≥n. [00000000000000000000000000000000] - la direcci√≥n de memoria m√°s baja. [1111111111111111111111111111111111] - la direcci√≥n de memoria m√°s alta.
  4. Como cada byte tiene una dirección de 32 bits, nuestro espacio de direcciones consta de 2 32 bytes direccionables (4 GB).

Como resultado, resulta que el n√ļmero de bytes direccionables depende del n√ļmero total de l√≠neas de direcci√≥n. Por ejemplo, si hay 64 l√≠neas de direcci√≥n (procesadores x86-64), puede direccionar 2 64 bytes (16 exabytes) de memoria, pero la mayor√≠a de las arquitecturas que usan punteros de 64 bits realmente usan l√≠neas de direcci√≥n de 48 bits (AMD64) y l√≠neas de direcciones de 42 bits (Intel), que te√≥ricamente permite que las computadoras est√©n equipadas con 256 terabytes de memoria f√≠sica (Linux permite, en la arquitectura x86-64, cuando se usan p√°ginas de direcciones de nivel 4, asignar hasta 128 TB de espacio de direcciones a los procesos, Windows le permite asignar hasta 192 TB).
Dado que el tama√Īo de la RAM f√≠sica es limitado, cada proceso se ejecuta en su propio "entorno limitado", en el denominado "espacio de direcciones virtuales", denominado memoria virtual.

Las direcciones de bytes en el espacio de direcciones virtuales no coinciden con las direcciones que utiliza el procesador para acceder a la memoria física. Como resultado, necesitamos un sistema que nos permita convertir direcciones virtuales en físicas. Eche un vistazo a cómo se ven las direcciones de memoria virtual.


Representación virtual del espacio de direcciones

Como resultado, cuando el procesador ejecuta una instrucción que se refiere a una dirección de memoria, el primer paso es traducir la dirección lógica a una dirección lineal. Esta conversión es realizada por la unidad de gestión de memoria.


Representación simplificada de la relación entre memoria virtual y física.

Dado que las direcciones l√≥gicas son demasiado grandes para ser convenientes para trabajar con ellas por separado (esto depende de varios factores), la memoria est√° organizada en estructuras llamadas p√°ginas. En este caso, el espacio de direcciones virtuales se divide en peque√Īas √°reas, p√°ginas, que en la mayor√≠a de los sistemas operativos tienen un tama√Īo de 4 KB, aunque generalmente este tama√Īo se puede cambiar. Esta es la unidad m√°s peque√Īa de administraci√≥n de memoria en la memoria virtual. La memoria virtual no almacena nada, simplemente establece la correspondencia entre el espacio de direcciones del programa y la memoria f√≠sica.

Los procesos solo ven las direcciones de memoria virtual. ¬ŅQu√© sucede si un programa necesita m√°s memoria din√°mica (tambi√©n llamada memoria de mont√≥n, o "mont√≥n")? Aqu√≠ hay un ejemplo de c√≥digo de ensamblador simple en el que se solicita memoria adicional asignada din√°micamente al sistema:

_start:        mov $12, %rax #    brk        mov $0, %rdi # 0 -  ,            syscall b0:        mov %rax, %rsi #  rsi    ,           mov %rax, %rdi #     ...        add $4, %rdi # ..  4 ,           mov $12, %rax #    brk        syscall 

Así es como se puede representar en forma de diagrama.


Aumenta la memoria asignada din√°micamente

El programa solicita memoria adicional utilizando la llamada al sistema brk (sbrk / mmap, etc.). El n√ļcleo actualiza la informaci√≥n sobre la memoria virtual, pero todav√≠a no se han presentado p√°ginas nuevas en la memoria f√≠sica, y aqu√≠ hay una diferencia entre la memoria virtual y la f√≠sica.

Asignador de memoria


Después de que, en términos generales, discutimos el trabajo con el espacio de direcciones virtuales, hablamos sobre cómo solicitar memoria dinámica adicional (memoria en el montón), nos será más fácil hablar sobre los medios para asignar memoria.

Si el mont√≥n tiene suficiente memoria para satisfacer nuestras solicitudes de c√≥digo, entonces el asignador de memoria puede ejecutar estas solicitudes sin acceder al n√ļcleo. De lo contrario, tiene que aumentar el tama√Īo del mont√≥n utilizando una llamada al sistema (utilizando brk, por ejemplo), mientras solicita un gran bloque de memoria. En el caso de malloc, "grande" significa el tama√Īo descrito por el par√°metro MMAP_THRESHOLD , que, por defecto, es de 128 Kb.

Sin embargo, un asignador de memoria tiene más responsabilidades que simplemente asignar memoria. Una de sus responsabilidades más importantes es reducir la fragmentación de la memoria interna y externa, y asignar bloques de memoria lo más rápido posible. Suponga que nuestro programa ejecuta secuencialmente solicitudes para asignar bloques continuos de memoria usando una función de la forma malloc(size) , después de lo cual esta memoria se libera usando una función de la forma free(pointer) .


Demostración de fragmentación externa

En el diagrama anterior, en el paso p4, no tenemos suficientes bloques de memoria ubicados secuencialmente para cumplir con la solicitud de asignación de seis de esos bloques, aunque la cantidad total de memoria libre lo permite. Esta situación lleva a la fragmentación de la memoria.

¬ŅC√≥mo reducir la fragmentaci√≥n de la memoria? La respuesta a esta pregunta depende del algoritmo de asignaci√≥n de memoria espec√≠fico, en el que se utiliza la biblioteca base para trabajar con la memoria.

Ahora veremos la herramienta de asignación de memoria TCMalloc, en la que se basan los mecanismos de asignación de memoria Go.

TCMalloc


TCMalloc se basa en la idea de dividir la memoria en varios niveles para reducir la fragmentación de la memoria. Dentro de TCMalloc, la administración de memoria se divide en dos partes: trabajar con memoria de subprocesos y trabajar con el montón.

‚Ėć Memoria de hilo


Cada p√°gina de memoria se divide en una secuencia de fragmentos de ciertos tama√Īos, seleccionados de acuerdo con las clases de tama√Īo. Esto reduce la fragmentaci√≥n. Como resultado, cada subproceso tiene a su disposici√≥n una memoria cach√© para objetos peque√Īos, lo que permite una asignaci√≥n muy eficiente de la memoria para objetos menores o iguales a 32 KB.


Flujo de caché

‚ĖćBunch


Un montón gestionado TCMalloc es una colección de páginas en las que un conjunto de páginas consecutivas se puede representar como un rango de páginas (intervalo). Cuando necesita asignar memoria para un objeto que es mayor que 32 KB, el montón se usa para asignar memoria.


Apila y trabaja con p√°ginas

Cuando no hay suficiente espacio para colocar peque√Īos objetos en la memoria, recurren al mont√≥n de memoria. Si el mont√≥n no tiene suficiente memoria libre, se solicita memoria adicional del sistema operativo.

Como resultado, el modelo presentado de trabajar con memoria admite el conjunto de memoria de espacio de usuario; su uso mejora significativamente la eficiencia de asignación y liberación de memoria.

Cabe se√Īalar que la herramienta de asignaci√≥n de memoria Go se bas√≥ originalmente en TCMalloc, pero difiere ligeramente de ella.

Ir asignador de memoria


Sabemos que el tiempo de ejecuci√≥n de Go planea ejecutar goroutines en procesadores l√≥gicos. De manera similar, la versi√≥n de TCMalloc utilizada por Go divide las p√°ginas de memoria en bloques cuyos tama√Īos corresponden a ciertas clases de tama√Īos de las cuales existen 67.

Si no está familiarizado con el planificador Go , puede leer sobre esto aquí .


Ir clases de tama√Īo

Dado que el tama√Īo m√≠nimo de p√°gina en Go es de 8192 bytes (8 Kb), si dicha p√°gina se divide en bloques de 1 KB, obtendremos 8 de esos bloques.


Un tama√Īo de p√°gina de 8 KB se divide en bloques correspondientes a un tama√Īo de clase de 1 KB

Secuencias de p√°gina similares en Go se controlan utilizando una estructura llamada mspan.

‚ĖćEstructura mspan


La estructura mspan es una lista doblemente vinculada, un objeto que contiene la direcci√≥n de inicio de la p√°gina, informaci√≥n sobre el tama√Īo de la p√°gina y el n√ļmero de p√°ginas incluidas en ella.


Estructura mspan

‚Ėć estructura mcache


Al igual que TCMalloc, Go proporciona a cada procesador lógico un caché de subprocesos local, conocido como mcache. Como resultado, si goroutine necesita memoria, puede obtenerla directamente de mcache. Para hacer esto, no necesita realizar bloqueos, ya que en un momento dado solo se ejecuta una goroutin en un procesador lógico.

La estructura mcache contiene, en forma de cach√©, estructuras mspan de varias clases de tama√Īo.


Interacción entre procesador lógico, mcache y mspan en Go

Dado que cada procesador lógico tiene su propio mcache, no hay necesidad de bloqueos al asignar memoria desde mcache.

Cada clase de tama√Īo se puede representar mediante uno de los siguientes objetos:

  • Un objeto de escaneo es un objeto que contiene un puntero.
  • Un objeto noscan es un objeto en el que no hay puntero.

Una de las fortalezas de este enfoque es que cuando se realiza la recolección de basura, no es necesario eludir los objetos noscan, ya que no contienen objetos para los que se asigna memoria.

¬ŅQu√© se mete en mcache? Los objetos cuyo tama√Īo no exceda los 32 KB van directamente a mcache usando mspan de la clase de tama√Īo correspondiente.

¬ŅQu√© sucede si mcache no tiene una c√©lula libre? Luego obtienen un nuevo mspan de la clase de tama√Īo deseada de la lista de objetos mspan llamada mcentral.

‚Ėć Estructura central


La estructura central recopila todos los rangos de p√°gina de una clase de tama√Īo particular. Cada objeto central contiene dos listas de objetos mspan.

  1. Lista de objetos mspan en los que no hay objetos libres, o aquellos mspan que est√°n en mcache.
  2. Lista de objetos mspan que tienen objetos libres.


Estructura central

Cada estructura central existe dentro de la estructura mheap.

‚Ėć Estructura del mapa


La estructura mheap está representada por un objeto que maneja la gestión del montón en Go. Solo hay un objeto global de este tipo que posee un espacio de direcciones virtual.


Estructura Mheap

Como puede ver en el diagrama anterior, la estructura mheap contiene una matriz de estructuras centrales. Este conjunto contiene estructuras centrales para todas las clases de tama√Īo.

 central [numSpanClasses]struct { mcentral mcentral   pad     [sys.CacheLineSize unsafe.Sizeof(mcentral{})%sys.CacheLineSize]byte } 

Dado que tenemos una estructura central para cada clase de tama√Īo, cuando mcache solicita la estructura mspan de mcentral, se aplica un bloqueo en el nivel central individual, como resultado, las solicitudes de otras estructuras mspan solicitantes de mcache de otros tama√Īos pueden ser atendidas al mismo tiempo.

La alineaci√≥n (almohadilla) asegura que las estructuras centrales est√©n separadas entre s√≠ por el n√ļmero de bytes correspondientes al valor CacheLineSize . Como resultado, cada mcentral.lock tiene su propia l√≠nea de cach√©, lo que evita los problemas asociados con el uso compartido de memoria falsa.

¬ŅQu√© sucede si la lista central est√° vac√≠a? Luego, mcentral recibe una secuencia de p√°ginas de mheap para asignar fragmentos de memoria de la clase de tama√Īo requerida.

  • free[_MaxMHeapList]mSpanList es una matriz de spanList. La estructura de mspan en cada spanList consta de 1 ~ 127 (_MaxMHeapList - 1) p√°ginas. Por ejemplo, free [3] es una lista vinculada de estructuras mspan que contiene 3 p√°ginas. La palabra "libre" en este caso indica que estamos hablando de una lista vac√≠a en la que no se asigna memoria. Una lista puede ser, en lugar de estar vac√≠a, una lista en la que se asigna memoria (ocupada).
  • freelarge mSpanList es una lista de estructuras de mspan gratuitas. El n√ļmero de p√°ginas por elemento (es decir, mspan) es superior a 127. Para admitir esta lista, se utiliza la estructura de datos mtreap. La lista de estructuras mspan ocupadas se llama busylarge.

Los objetos de más de 32 Kb se consideran objetos grandes, la memoria para ellos se asigna directamente desde mheap. Las solicitudes para asignar memoria para tales objetos se realizan mediante un bloqueo, como resultado, en un momento dado, una solicitud similar puede procesarse desde un solo procesador lógico.

El proceso de asignación de memoria para objetos.


  • Si el tama√Īo del objeto excede los 32 Kb, se considera grande, la memoria para √©l se asigna directamente desde mheap.
  • Si el tama√Īo del objeto es inferior a 16 Kb, se utiliza el mecanismo de mcache llamado peque√Īo asignador.
  • Si el tama√Īo del objeto est√° en el rango de 16-32 Kb, resulta qu√© clase de tama√Īo (sizeClass) usar, entonces se asigna un bloque adecuado en mcache.
  • Si no hay bloques disponibles en la clase de tama√Īo correspondiente a mcache, se llama a mcentral.
  • Si mcentral no tiene bloques libres, llaman a mheap y buscan el mspan m√°s adecuado. Si el tama√Īo de memoria requerido por la aplicaci√≥n resulta ser m√°s grande de lo que se puede asignar, el tama√Īo de memoria solicitado se procesar√° para que sea posible devolver tantas p√°ginas como sea necesario por el programa, formando una nueva estructura de mspan.
  • Si la memoria virtual de la aplicaci√≥n a√ļn no es suficiente, se accede al sistema operativo para un nuevo conjunto de p√°ginas (se requiere al menos 1 MB de memoria).

De hecho, a nivel del sistema operativo, Go solicita la asignaci√≥n de piezas de memoria a√ļn m√°s grandes llamadas arenas. La asignaci√≥n simult√°nea de grandes fragmentos de memoria le permite encontrar un compromiso entre la cantidad de memoria asignada a la aplicaci√≥n y el costoso acceso al sistema operativo en t√©rminos de rendimiento.

La memoria solicitada en el montón se asigna desde la arena. Considere este mecanismo.

Memoria virtual ir


Eche un vistazo al uso de la memoria con un programa simple escrito en Go:

 func main() {   for {} } 


Información del proceso del programa

El espacio de direcciones virtuales de incluso un programa tan simple es de aproximadamente 100 MB, mientras que el índice RSS es de solo 696 Kb. Primero, intentemos averiguar el motivo de esta discrepancia.


Información de mapas y smap

Aqu√≠ puede ver las √°reas de memoria, cuyo tama√Īo es aproximadamente igual a 2 MB, 64 MB, 32 MB. ¬ŅQu√© tipo de memoria es esta?

‚ĖćArena


Resulta que la memoria virtual en Go consiste en un conjunto de arenas. El tama√Īo de memoria inicial destinado al almacenamiento din√°mico corresponde a una arena, es decir, 64 MB (esto es relevante para Go 1.11.5).


Tama√Īo actual de arena en varios sistemas

Como resultado, la memoria necesaria para las necesidades actuales del programa se asigna en peque√Īas porciones. Este proceso comienza con una arena de 64 MB.

Esos indicadores num√©ricos de los que estamos hablando aqu√≠ no deben tomarse para algunos valores absolutos y sin cambios. Pueden cambiar. Anteriormente, por ejemplo, Go reserv√≥ un espacio virtual continuo por adelantado, en sistemas de 64 bits el tama√Īo de la arena era 512 GB (es interesante pensar en lo que sucede si la demanda de memoria real es tan grande que mmap rechazar√° la solicitud correspondiente).

De hecho, llamamos a un mont√≥n de arenas un mont√≥n. En Go, las arenas se perciben como fragmentos de memoria, divididos en bloques de 8192 bytes (8 Kb) de tama√Īo.


Una arena de 64 MB

Go tiene un par de sabores más de bloques: span y mapa de bits. La memoria para ellos se asigna fuera del montón, almacenan metadatos de arena. Se utilizan principalmente en la recolección de basura.
Aquí hay un resumen general de cómo funcionan los mecanismos de asignación de memoria en Go.


Esquema general de los mecanismos de asignación de memoria en Go

Resumen


En general, se puede observar que en este material describimos los subsistemas para trabajar con la memoria Go en t√©rminos muy generales. La idea principal del subsistema de memoria en Go es asignar memoria usando varias estructuras y cach√©s de diferentes niveles. Esto tiene en cuenta el tama√Īo de los objetos para los que se asigna memoria.

La representaci√≥n de un solo bloque de direcciones de memoria continua recibidas del sistema operativo en forma de una estructura multinivel aumenta la eficiencia del mecanismo de asignaci√≥n de memoria debido al hecho de que este enfoque evita el bloqueo. La asignaci√≥n de recursos, teniendo en cuenta el tama√Īo de los objetos que deben almacenarse en la memoria, reduce la fragmentaci√≥n y, despu√©s de liberar memoria, le permite acelerar la recolecci√≥n de basura.

Estimados lectores! ¬ŅHa encontrado problemas causados ‚Äč‚Äčpor un mal funcionamiento de la memoria en los programas escritos en Go?

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


All Articles