El ocio de una taza de té verde que el runtime de Go nos arrebata, GreenTea GC
¿Por qué un nuevo GC?
¿Por qué el GC existente?
El GC actual de Go puede describirse con las siguientes afirmaciones:
- concurrent tri-color mark and sweep (marcado y barrido concurrente de tres colores)
- Utiliza un total de tres colores (blanco, gris y negro) para rastrear el estado de los objetos.
- Blanco: Objeto aún no visitado.
- Gris: Objeto visitado, pero no todos sus objetos hijos han sido visitados.
- Negro: Todos los objetos hijos han sido visitados.
- Una vez finalizada la iteración, todos los objetos blancos son recolectados.
- Utiliza Write Barrier (barrera de escritura) para proteger la memoria recién creada durante el GC.
- El tiempo de activación/desactivación del Write Barrier constituye una parte significativa del comúnmente denominado Stop-The-World (en adelante, STW).
- Realiza este proceso de forma concurrente.
- Utiliza un total de tres colores (blanco, gris y negro) para rastrear el estado de los objetos.
- No Compaction (Sin compactación)
- El GC de Go no realiza compactación.
- Puede ocurrir fragmentación de la memoria.
- Sin embargo, los objetos de menos de 32 kb minimizan la fragmentación al utilizar la caché por-P del asignador (Allocator) de memoria del lenguaje Go.
- Non-Generational (No generacional)
- El GC de Go no gestiona los objetos por generaciones.
- Todos los objetos pertenecen a la misma generación.
- Escape Analysis (Análisis de escape)
- Go determina si un objeto se asigna en el heap o en el stack mediante Escape Analysis.
- A grandes rasgos, si se utilizan dangling pointers (punteros colgantes) o interfaces, se puede considerar que se asigna en el heap.
Lo importante es
El GC de Go se caracteriza por recorrer todos los objetos desde la raíz y realizar el marcado de tres colores. Este proceso puede resumirse en una frase como algoritmo de exploración concurrente de gráficos con ordenamiento topológico
. Sin embargo, es probable que cada objeto resida en diferentes regiones de memoria. Hablando de forma simple:
- Supongamos que hay dos regiones de memoria separadas por unos 32MB.
- En estas dos regiones de memoria hay 5 objetos asignados en cada una.
- Los objetos A, B, C están en la región 1.
- Los objetos D, E están en la región 2.
- El orden de referencia de los objetos es
A -> D -> B -> E -> C
. - Cuando el GC comienza, visita A, luego D, luego B, luego E y finalmente C.
- En este momento, dado que A y D residen en diferentes regiones de memoria, el proceso de visitar D después de A requiere moverse entre regiones de memoria.
- En este proceso, se incurre en un costo de salto de memoria, como el movimiento entre regiones de memoria y el llenado de nuevas líneas de caché. Una parte considerable del tiempo de CPU utilizado por la aplicación puede asignarse a estas tareas del GC.
- Este costo persiste mientras el GC está en curso.
Entonces, ¿cuál es el problema?
El comportamiento de este GC es perjudicial en los siguientes casos:
- Cuando hay muchos núcleos y mucha memoria.
- El GC de Go se ejecuta concurrentemente por defecto.
- Sin embargo, si la región de memoria es amplia y los objetos están dispersos, múltiples núcleos explorarán objetos simultáneamente en diversos espacios de memoria.
- Si el bus entre la CPU y la memoria no es lo suficientemente grande en este proceso, el ancho de banda de la memoria puede convertirse en un cuello de botella.
- Además, debido a la distancia física real, cada exploración individual implicará un retraso comparativamente grande.
- Cuando hay muchos objetos pequeños.
- Si se gestionan árboles o gráficos con mucha profundidad o muchos hijos en Go, el GC debe explorar todos estos objetos.
- Si los objetos están dispersos en diferentes regiones de memoria durante este proceso, se producirá un retraso debido a la primera razón.
- Además, si hay muchos objetos pequeños, el GC debe explorarlos todos, por lo que el núcleo de la CPU asignará una cantidad considerable de su capacidad al GC mientras este se ejecuta y trabajará en ello.
Para resolver estos problemas, el equipo de Golang ha anunciado el GreenTea GC.
Ya no tienes tiempo para más té verde
¿Qué es lo que quita el té verde?
El área donde se consideró que se podía aplicar la solución más rápida al GC existente fue la localidad de la memoria. Es decir, minimizar los saltos de memoria haciendo que los objetos se ubiquen cerca unos de otros. Sin embargo, no se puede forzar el patrón del programador que escribe el código, y la asignación de objetos es impredecible según el flujo de trabajo.
Por lo tanto, el método elegido por el equipo de Golang es el Memory Span (Tramo de Memoria).
¿Qué es un Memory Span?
Un Memory Span es una región de memoria comparativamente grande que se asigna para acomodar objetos pequeños. Y el nuevo GC, denominado GreenTea GC, realiza la recolección de basura en este Memory Span. El funcionamiento detallado es casi idéntico al Tri-Color Mark and Sweep existente.
Funcionamiento del GreenTea GC
Primero, el GreenTea GC asigna un Memory Span. Como se mencionó anteriormente, su tamaño es de 8 KiB, un tamaño considerable. Y dentro de este, se pueden asignar objetos de un tamaño máximo de 512 bytes. Este tamaño es aproximadamente el de un nodo de árbol o gráfico, o el tamaño de una struct que escapa al heap, por lo que es difícil que el tamaño sea mayor. Cada vez que un objeto de 512 bytes o menos escapa al heap, se acumula en este Memory Span, y cuando este se llena, se asigna un nuevo Memory Span.
Ahora, cuando ocurre un GC, el GreenTea GC apila estos Memory Spans en una cola y los inspecciona secuencialmente. En este proceso, el GreenTea GC utiliza una programación muy similar al modelo GMP existente. También se implementa el robo de trabajo (work stealing). En cualquier caso, el worker que saca un Memory Span de la cola inspecciona los objetos internos del Memory Span que le ha sido asignado. En este proceso, se utiliza el Tri-Color Mark and Sweep de manera idéntica.
¿Qué ventaja tiene esto?
La diferencia principal con el GC existente se reduce a una: la unidad sobre la que se realiza el GC a la vez ha pasado de ser el objeto a ser el Memory Span. Gracias a esto, el GC obtiene las siguientes ventajas:
- Localidad de la memoria: Dado que los objetos están agrupados en el Memory Span, el salto de memoria se minimiza al explorar los objetos. Es decir, se puede maximizar el uso de la caché de la CPU.
- Mejora del rendimiento del GC: Al realizar el GC por unidad de Memory Span, el GC funciona de manera más eficiente en entornos multi-núcleo.
- Optimización de la asignación de memoria: Dado que el Memory Span se asigna con un tamaño fijo, se reduce la sobrecarga de la asignación de memoria para objetos pequeños y disminuye la posibilidad de fragmentación. Esto aumenta la eficiencia de la liberación de la asignación de memoria.
En resumen, ahora podemos asignar objetos de 512 bytes o menos con más frecuencia y con mayor tranquilidad.
Sin embargo, también hay escenarios donde esto es particularmente ventajoso:
- Estructuras de árbol/gráfico: En casos con alto fan-out y donde los cambios ocurren con poca frecuencia.
- Árbol B+, Trie, AST (Abstract Syntax Tree)
- Estructuras de datos amigables con la caché.
- Procesamiento por lotes: Flujos de trabajo de asignación masiva de datos pequeños.
- Múltiples objetos pequeños creados por el análisis JSON.
- Objetos DAO de conjuntos de resultados de bases de datos.
En estos casos, el GreenTea GC puede ofrecer un mejor rendimiento que el GC existente. Sin embargo, no se aplica a todos los casos, y aún es difícil esperar una mejora de rendimiento dramática, especialmente cuando los objetos están dispersos en diferentes regiones de memoria.
Por lo tanto
Parece que el equipo de Golang tiene la intención de seguir mejorando el GC a largo plazo. Este GreenTea GC puede considerarse un cambio menor en un área pequeña y no reemplaza al GC existente. Sin embargo, el GreenTea GC parece un caso interesante que permite vislumbrar los problemas que el equipo de Golang enfrenta o anticipa. También fue impresionante el intento de resolver un problema sorprendentemente complejo con la adición de un concepto relativamente simple. Personalmente, lo considero un caso interesante.
El GreenTea GC es una característica experimental que se introducirá a partir de Go 1.25. Se puede activar utilizando la variable de entorno GOEXPERIMENT=greenteagc
. Dado que esta característica es aún experimental, se requiere una prueba exhaustiva antes de usarla en un entorno de producción.