¿Cómo un recolector de basura evita que se escanee toda la memoria en cada recolección?

16

Algunos recolectores de basura (al menos Mono y .NET) tienen un área de memoria a corto plazo que escanean con frecuencia y un área de memoria secundaria que escanean con menos frecuencia. Mono llama a esto una guardería.

Para averiguar qué objetos se pueden eliminar, escanean todos los objetos a partir de las raíces, la pila y los registros, y eliminan todos los objetos a los que ya no se hace referencia.

Mi pregunta es cómo evitan que se escanee toda la memoria en uso en cada recopilación. En principio, la única forma de averiguar qué objetos ya no se usan es escanear todos los objetos y todas sus referencias. Sin embargo, esto evitaría que el sistema operativo intercambie memoria a pesar de que la aplicación no lo utiliza y se siente como una gran cantidad de trabajo que debe hacerse, también para "Nursery Collection". No parece que estén ganando mucho al usar una guardería.

¿Me estoy perdiendo algo o el recolector de basura realmente escanea cada objeto y cada referencia cada vez que hace una recolección?

Pieter van Ginkel
fuente
1
una buena descripción se encuentra en un artículo The Art of Garbage Collection Tuning escrito por Angelika Langer. Formalmente, se trata de la forma en que se hace en Java, pero los conceptos presentados son bastante agnósticos al lenguaje
gnat

Respuestas:

14

Las observaciones fundamentales que permiten que la recolección de basura generacional evite tener que escanear todos los objetos de generaciones anteriores son:

  1. Después de una colección, todos los objetos que aún existan serán de una generación mínima (por ejemplo, en .net, después de una colección Gen0, todos los objetos son Gen1 o Gen2; después de una colección Gen1 o Gen2, todos los objetos son Gen2).
  2. Un objeto, o parte del mismo, que no se ha escrito desde una colección que promovió todo a la generación N o superior no puede contener referencias a objetos de generaciones inferiores.
  3. Si un objeto ha alcanzado una determinada generación, no necesita identificarse como accesible para garantizar su retención cuando se recolectan generaciones más bajas.

En muchos marcos de trabajo de GC, es posible que el recolector de basura marque objetos o partes de ellos de tal manera que el primer intento de escribir en ellos active un código especial para registrar el hecho de que se han modificado. Un objeto o parte del mismo que ha sido modificado, independientemente de su generación, debe escanearse en la próxima colección, ya que puede contener referencias a objetos más nuevos. Por otro lado, es muy común que haya muchos objetos antiguos que no se modifiquen entre colecciones. El hecho de que los escaneos de baja generación puedan ignorar tales objetos puede permitir que dichos escaneos se completen mucho más rápidamente de lo que lo harían de otra manera.

Tenga en cuenta, por cierto, que incluso si uno no puede detectar cuándo se modifican los objetos y tendría que escanear todo en cada paso del GC, la recolección de basura generacional aún podría mejorar el rendimiento de la etapa de "barrido" de un recolector de compactación. En algunos entornos integrados (especialmente aquellos en los que hay poca o ninguna diferencia de velocidad entre los accesos de memoria secuenciales y aleatorios), mover bloques de memoria es relativamente costoso en comparación con las referencias de etiquetado. En consecuencia, incluso si la fase de "marca" no se puede acelerar utilizando un colector generacional, puede valer la pena acelerar la fase de "barrido".

Super gato
fuente
mover bloques de memoria es costoso en cualquier sistema, por lo que mejorar el barrido es una ganancia incluso en su sistema de CPU quad Ghz.
gbjbaanb
@gbjbaanb: En muchos casos, el costo de escanear todo para encontrar objetos vivos sería significativo y objetable incluso si mover los objetos fuera totalmente gratuito. En consecuencia, cuando sea práctico, se debe evitar escanear objetos viejos. Por otro lado, abstenerse de compactar objetos antiguos es una optimización simple que se puede lograr incluso en marcos simples. Por cierto, si uno estuviera diseñando un marco GC para un sistema embebido pequeño, el soporte declarativo para objetos inmutables podría ser útil. El seguimiento de si un objeto mutable ha cambiado es difícil, pero uno podría hacer bien en ...
supercat
... simplemente asuma que los objetos mutables necesitan ser escaneados en cada pase GC pero los objetos inmutables no. Incluso si la única forma de construir un objeto inmutable fuera construir un "prototipo" en un espacio mutable y luego copiarlo, la única operación de copia adicional podría evitar la necesidad de escanear el objeto en futuras operaciones de GC.
supercat
Por cierto, el rendimiento de la recolección de basura en las implementaciones de BASIC derivadas de Microsoft de 1980 para microprocesadores 6502 (y quizás otros también) podría mejorarse en algunos casos, si un programa que generara muchas cadenas que nunca cambiarían, copiara el "siguiente asignación de cadena "al puntero" parte superior del espacio de cadena ". Tal cambio evitaría que el recolector de basura examine cualquiera de las viejas cadenas para ver si aún se necesitaban. El Commodore 64 no era de alta tecnología, pero ese GC "generacional" ayudaría incluso allí.
supercat
7

Los GC a los que se refiere son recolectores de basura generacionales . Están diseñados para aprovechar al máximo una observación conocida como "mortalidad infantil" o "hipótesis generacional", lo que significa que la mayoría de los objetos se vuelven inalcanzables muy rápidamente. De hecho, escanean a partir de las raíces, pero ignoran todos los objetos antiguos . Por lo tanto, no necesitan escanear la mayoría de los objetos en la memoria, solo escanean objetos jóvenes (a expensas de no detectar objetos viejos inalcanzables, al menos no en ese punto).

"Pero eso está mal", te escucho gritar, "los objetos viejos pueden referirse y se refieren a objetos jóvenes". Tiene razón, y hay varias soluciones para eso, que giran en torno a la obtención de conocimiento, de manera rápida y eficiente, qué objetos antiguos deben verificarse y cuáles son seguros de ignorar. Se reducen prácticamente a objetos de grabación, o pequeños rangos de memoria (más grandes que los objetos, pero mucho más pequeños que todo el montón) que contienen punteros a las generaciones más jóvenes. Otros lo han descrito mucho mejor que yo, así que solo le daré un par de palabras clave: marcado de tarjetas, conjuntos recordados, barreras de escritura. También hay otras técnicas (incluidos los híbridos), pero estas abarcan los enfoques comunes que conozco.


fuente
3

Para averiguar qué objetos de guardería aún están vivos, el recolector solo necesita escanear el conjunto raíz y cualquier objeto antiguo que haya sido mutado desde la última colección , ya que un objeto antiguo que no ha sido mutado recientemente no puede apuntar a un objeto joven . Existen diferentes algoritmos para mantener esta información en diferentes niveles de precisión (desde un conjunto exacto de campos mutados a un conjunto de páginas donde puede haber ocurrido una mutación), pero todos generalmente involucran algún tipo de barrera de escritura : código que se ejecuta en cada referencia de tipo de campo que actualiza la contabilidad del GC.

Ryan Culpepper
fuente
1

La generación más antigua y más simple de recolectores de basura realmente escaneó toda la memoria y tuvo que detener el resto del procesamiento mientras lo hacían. Los algoritmos posteriores mejoraron esto de varias maneras, haciendo que la copia / escaneo sea incremental o se ejecute en paralelo. La mayoría de los recolectores de basura modernos segregan los objetos en generaciones, y manejan cuidadosamente los punteros intergeneracionales para que las nuevas generaciones puedan ser recolectadas sin molestar a las más antiguas.

El punto clave es que los recolectores de basura trabajan en estrecha colaboración con el compilador y con el resto del tiempo de ejecución para mantener la ilusión de que está observando toda la memoria.

ddyer
fuente
No estoy seguro de qué enfoques de recolección de basura se usaron en minicomputadoras y mainframes antes de fines de la década de 1970, pero el recolector de basura BASIC de Microsoft, al menos en máquinas 6502, establecería su puntero de "siguiente cadena" en la parte superior de la memoria, y luego buscaría todas las referencias de cadena para encontrar la dirección más alta que estaba debajo del "siguiente puntero de cadena". Esa cadena se copiaría justo debajo del "siguiente puntero de cadena", y ese puntero se estacionaría justo debajo de él. El algoritmo luego se repetiría. Era posible que el código jinx los punteros para proporcionar ...
supercat
... algo así como la colección generacional. A veces me he preguntado lo difícil que sería parchear el BASIC para implementar una colección "generacional" simplemente manteniendo las direcciones de la parte superior de cada generación y agregando algunas operaciones de intercambio de puntero antes y después de cada ciclo de GC. El rendimiento del GC seguiría siendo bastante malo, pero en muchos casos podría reducirse de decenas de segundos a décimas de segundo.
supercat
-2

Básicamente ... GC usa "cubos" para separar lo que está en uso y lo que no. Una vez que lo hace, borra las cosas que no están en uso y mueve todo lo demás a la 2da generación (que se verifica con menos frecuencia que la 1ra generación) y luego mueve las cosas que todavía están en uso en la 2da den a la 3ra generación.

Entonces, las cosas en la tercera generación generalmente son objetos que están atascados por alguna razón, y GC no revisa allí muy a menudo.

aserwin
fuente
1
Pero, ¿cómo sabe qué objetos están en uso?
Pieter van Ginkel
Realiza un seguimiento de los objetos a los que se puede acceder desde un código accesible. Una vez que un objeto ya no es accesible desde cualquier código que pueda ejecutarse (por ejemplo, código para un método que ha regresado), entonces el GC sabe que es seguro recolectarlo
JohnL
Ustedes dos están describiendo cómo los GC son correctos, no cómo son eficientes. A juzgar por la pregunta, OP lo sabe muy bien.
@delnan sí, estaba respondiendo a la pregunta de cómo sabe qué objetos están en uso, que es lo que estaba en el comentario de Pieter.
JohnL
-5

El algoritmo generalmente utilizado por este GC es el marcado y barrido Naive

También debe tener en cuenta el hecho de que esto no es administrado por el C # en sí, sino por el llamado CLR .

usuario827992
fuente
Esa es la sensación que tuve al leer sobre el recolector de basura de Mono. Sin embargo, lo que no entiendo es por qué si están escaneando el conjunto de trabajo completo en una colección, tienen un colector generacional con el que la colección GEN-0 es muy rápida. ¿Cómo puede ser esto rápido con un conjunto de trabajo de digamos 2GB?
Pieter van Ginkel
bueno, el GC real para mono es Sgen, deberías leer este mono-project.com/Generational_GC o algunos artículos en línea schani.wordpress.com/tag/mono infoq.com/news/2011/01/SGen , el punto es que Estas nuevas tecnologías como CLR y CLI tienen un diseño realmente modular, el lenguaje se convierte en una forma de expresar algo para el CLR y no en una forma de producir código binario. Su pregunta es sobre detalles de implementación y no sobre algoritmos, ya que un algoritmo aún no tiene una implementación, solo debe leer documentos técnicos y artículos de Mono, nadie más.
user827992
Estoy confundido. ¿La estrategia que utiliza un recolector de basura no es un algoritmo?
Pieter van Ginkel
2
-1 Deja de confundir OP. Que el GC es parte del CLR y no específico del idioma no es relevante en absoluto. Un GC se caracteriza principalmente por la forma en que establece el montón y determina la accesibilidad, y el último es todo sobre el algoritmo (s) que se utiliza para eso. Si bien puede haber muchas implementaciones de un algoritmo, y no debe quedar atrapado en los detalles de la implementación, el algoritmo solo determina cuántos objetos se escanean. Un GC generacional es simplemente un algoritmo + diseño de montón que intenta utilizar la "hipótesis generacional" (que la mayoría de los objetos mueren jóvenes). Estos no son ingenuos.
44
Algorithm! = Implementación de hecho, pero una implementación solo puede desviarse hasta ahora antes de convertirse en una implementación de un algoritmo diferente. La descripción de un algoritmo, en el mundo de GC, es muy específica e incluye cosas como no escanear todo el montón en la colección de vivero y cómo se encuentran y almacenan los punteros intergeneracionales. Es cierto que un algoritmo no le dice cuánto tiempo llevará un paso específico del algoritmo, pero eso no es relevante en absoluto para esta pregunta.