Garbage Collection

Barrenderos de bits, motines virtuales y otros cuentos fantásticos

C++                                                     C#

Encuentre las 10 diferencias...

(PD: no vale usar smart pointers -por ahora-)

int foo() {
    size_t n = read_size();
    int *elements = malloc(n * sizeof(int));
    
    if (read_elements(n, elements) < n) {
    	// oops...
    	return -1;
    }

    free(elements)
    return 0;
}
private int foo()
{
    return read_elements().Count() < read_size()
                           ? -1
		           : 0;
}

Comparaciones odiosas: parte I

C++

  • Compilación nativa
  • Código dependiente de plataforma y CPU (preprocessor hell)
  • Gran variedad de compiladores y tooling (Clang, LLVM, GCC, MSVC...)
  • Gran performance, binarios livianos, linking lento

C#

  • Compilación a IL -> JIT
  • Código agnóstico de la plataforma, corre sobre una VM
  • Actualmente solo 4 implementaciones de CLR, (desktop, coreclr/corert y mono)
  • Menor performance que lenguajes nativos (mas overhead y binarios mas pesados)

Comparaciones odiosas: parte II

Nos olvidamos de algo? Hay que hacer...

 

Memoria

Memoria: un tema olvidado

  • El manejo manual de memoria es tedioso y peligroso
  • Se ha vuelto dispensable, salvo en nichos particulares (gaming, real time, embedded)
  • Mayor disponibilidad de memoria hoy en día nos ahorra la necesidad de exprimir cada bit vía código
  • Los lenguajes modernos (salvo excepciones discutibles como D, Rust) siguen esta tendencia de abstraer el manejo de memoria

 

Como consecuencia, gran parte de los desarrolladores de ultima generación solo ha trabajado con memoria en el ámbito académico y no de forma profesional

Lenguajes unmanaged

(no gestionados)

Los lenguajes clásicos (tales como C y C++) permiten (o más bien, obligan) al programador a manejar de forma explicita la memoria (alocación y liberación)

 

Esto ademas implica que la seguridad de tipos no esta asegurada (valga la redundancia) ya que es responsabilidad nuestra el manejo de punteros a objetos

Bugs en memoria manual

Fallas humanas en el manejo manual de memoria nos pueden abrir la puerta a  bestias indomables y difíciles de debuggear tales como:

 

  • Memory leaks (fugas de memoria): Sucede cuando nos olvidamos de liberar memoria alocada
  • Double frees: Sucede al liberar 2 veces la misma región de memoria
  • Use-After-Free: Ocurren como consecuencia de utilizar un area de memoria ya liberada

 

Los últimos 2 conforman enormes riesgos de seguridad

Lenguajes managed

(gestionados)

Este (no tan) nuevo tipo de lenguajes tiene como principal característica un manejo automático de memoria, valiéndose de subsistemas para ello

 

El manejo automático de memoria también nos permite tener un tipado más seguro

Desventajas en memoria automática

  • Mas overhead y penalizaciones de performance
  • No se puede gestionar (usualmente) de manera manual la memoria, quedamos a la merced del compilador y/o entorno de ejecución
  • El programador no puede reclamar explicitamente los recursos no utilizados
  • Minimiza pero no erradica las fugas de memoria

 

 

El problema de la basura

Basura y bits

Las áreas de memoria alocada que ya no se utilizan se conocen como basura (o garbage)

 

Durante la ejecución de un programa, un objeto pasa a considerarse basura si queda sin referencias

 

El problema de determinar si una región de memoria es basura o no es indecidible, por lo que los algoritmos existentes actúan por aproximación

El manejo de memoria depende de complejos subsistemas, lo cual nos lleva a...

Garbage Collection

Historia

Método desarrollado por John McCarthy en 1958-1959 como mecanismo de manejo de memoria de LISP

El primer GC fue Mark & Sweep, bloqueante

Actualmente decenas de lenguajes poseen o dependen de variadas técnicas de garbage collection para su gestión de memoria

Objetivos

Un garbage collector (de ahora en adelante, GC) debe realizar las siguientes tareas:

 

  • Alocación de memoria
  • Detección de basura
  • Dealocación y limpieza de la memoria recolectada
  • Opcional: Compactar la memoria todavía en uso

La guerra de los trade-offs

Para que sea eficiente, a la vez, se necesita que:

  • Las recolecciones ocurran con una frecuencia tal que no se sature la memoria alocada...
  • ...pero que también ocurran con poca frecuencia para evitar uso intensivo de CPU

 

  • Reclamen una cantidad de memoria suficiente para que la recolección haya valido la pena...
  • ...pero que sean de corta duración

 

  • Minimicen o eliminen la necesidad por parte del desarrollador de operarla manualmente

Los garbage collectors generalmente son NO deterministas

Tipos de GC

Hay 2 tipos primarios de garbage collector:

 

  • Reference counting: Se hace un seguimiento del conteo de referencias de cada objeto. También llamado GC cooperativo

 

  • Tracing: Se inducen ciclos de marcado en los cuales se identifican los objetos referenciados, a fin de recolectar los que no poseen marcas. También llamado GC no cooperativo

¿Cual es el mejor?

Detectando la basura

GC Roots: son las "raices" que referencian a un objeto, usualmente conformadas por:

  • Almacenamiento global/estático
  • Almacenamiento local en threads

 

En lenguajes de tipado seguro, una vez que un objeto es inalcanzable (queda sin raíz) no se puede volver a referenciar desde la aplicación

Un objeto esta vivo si será accedido en algún momento desde la aplicación. Este factor es indecidible, pero la accesibilidad de punteros lo es

Vivos, muertos y zombies

Algoritmos de GC

Detección de basura (cont.)

La deteccion de basura y accesibilidad de objetos es el otro pilar de un GC. Hay varios algoritmos de detección y recolección, siendo los principales a saber:

 

  • Reference counting
  • Mark & Sweep/Compact
  • Stop & Copy/Semispace
  • Generacional

 

En varios casos se combinan técnicas. Ej: los GC de Java y .NET son Mark & Sweep generacionales

Reference Counting

  • Conteo de referencias por objeto:
    • ptr = x: refcount(x)++
    • ptr = y: refcount(x)--,refcount(y)++
  • Si un objecto tiene refcount = 0, se considera inalcanzable y se elimina

Las referencias cíclicas causan fugas de memoria

Ventajas

  • Fácil de implementar
  • Permite recolecciones inmediatas

 

Desventajas

  • No es capaz de detectar referencias cíclicas, las cuales nunca serán reclamadas
  • No hay compactación
  • Menor performance en asignaciones y multithread

Mark & Sweep/Compact

Se basa en la asunción de que todos los objetos rastreables desde los GC roots son alcanzables, mientras que el resto son considerados basura

 

2 (+1) fases

  • Mark: Se recorren todas las referencias alcanzables y se las marca como tal
  • Sweep: Se recolectan las referencias no marcadas, y se desmarcan aquellas que sí lo estaban
  • Compact (opcional): En variantes del algorimo se compacta la memoria en uso, evitando la fragmentación excesiva
  1. Inicio con todos los objetos como basura
  2. Mark: Recorrido y marcado de objetos vivos
  3. Sweep: limpieza de basura + compact (si aplica)

Ventajas

  • No requiere soporte de la aplicación ni el compilador
  • Maneja referencias cíclicas
  • Mark & Compact: Compactación, evita fragmentación

 

Desventajas

  • Pausas largas al recorrer todo el heap
  • Mark & Sweep: La fragmentación puede causar Out Of Memory prematuro
  • Mark & Compact: Overhead en copia de objetos y actualización de referencias

Stop & Copy

También conocido como semispace, consiste en la copia de la memoria en uso una región de memoria libre y contigua, garantizando la compactación además de la limpieza

 

  • El heap se divide en 2 mitades, una activa y otra inactiva
  • Al llenarse una, se copian los objetos referenciados a la otra (previamente limpiada), intercambiándose los roles

El uso de ambos espacios es turnado, a medida que se llena el usado actualmente

Ventajas

  • No hay fragmentación de memoria

Desventajas

 

Desventajas

  • Overhead en copia de objetos y actualización de referencias
  • Los objetos longevos se copian reiteradamente
  • Requiere el doble de memoria al tener un heap bipartito

Generacional

Basado en la premisa de que gran parte de los objetos muere joven

 

  • Heap dividido en generaciones (usualmente 2 o 3)
  • Los objetos nuevos caen en gen 0 (también llamada nursery)
  • Al iniciarse un ciclo se realiza sobre gen 0, y los objetos sobrevivientes se promueven a gen 1. Si el trigger continúa sobre gen 1 se recolecta de igual forma
  • Al llegar a la generación final, la misma se compacta

Los ciclos de recolección pueden abarcar algunas generaciones...

...o todas, si el trigger se propaga hasta la generación mas longeva

Ventajas

  • Ciclos de recolección cortos (mayoría de gen 0)
  • Solo se copian objetos longevos
  • No se recorre todo el heap

 

Desventajas

  • Overhead en copia de objetos y actualización de referencias

Ya recorrimos los algoritmos clásicos... pero momento!

Aún hay más

Incremental vs. concurrente vs. stop-the-world:

  • Un GC incremental ejecuta un ciclo en fases discretas, induciendo pausas cortas
  • Un GC bloqueante (stop-the-world) aplica pausas para realizar un ciclo completo
  • Un GC concurrente ejecuta los ciclos sin inducir pausas, o provocando pausas ínfimas (stack walking)

 

Compactante vs. no compactante

  • Un GC compactante defragmenta la memoria al finalizar cada recolección. Esto puede depender de heurísticas
  • Un GC no compactante deja la memoria en uso intacta, induciendo pausas cortas pero causando fragmentación

La guerra de los trade-offs (cont)

Puntos de mejora

  • Paralelismo (GC multithread)
  • Heurísticas para mejorar la eficiencia y eficacia de los triggers
  • Algoritmos sofisticados de recorrido y marcado de referencias (tri-color)
  • Estrategias de alocación (localidad, agrupamiento, atomicidad)
Made with Slides.com