¿Usar una tabla hash en la recolección de basura resolvería el problema de detener el mundo de marcar y barrer?

13

En el algoritmo de recolección de basura mark-sweep-compact, debe detener el mundo al reubicar objetos porque el gráfico de referencia se vuelve inconsistente y debe reemplazar los valores de todas las referencias que apuntan al objeto.

Pero, ¿qué pasaría si tuviera una tabla hash con ID de objeto como clave y puntero como valor, y las referencias apuntaran a dicha ID en lugar de la dirección del objeto ... luego, para corregir las referencias solo sería necesario cambiar un valor y la pausa solo sería necesaria si el objeto se intenta escribir durante la copia ...

¿Hay algún error en mi línea de pensamiento?

mrpyo
fuente

Respuestas:

19

Actualizar referencias no es lo único que requiere una pausa. Los algoritmos estándar comúnmente agrupados en "mark-sweep" suponen que todo el gráfico del objeto permanece inalterado mientras se marca. El manejo correcto de las modificaciones (nuevos objetos creados, referencias modificadas) requiere algoritmos alternativos bastante complicados, como el algoritmo tricolor. El término general es "recolección de basura concurrente".

Pero sí, la actualización de referencias después de la compactación también necesita una pausa. Y sí, el uso indirecto (por ejemplo, a través de una identificación de objeto persistente y una tabla hash para punteros reales) puede reducir en gran medida la pausa. Incluso podría ser posible hacer que esta parte esté libre de bloqueo si así lo desea. Seguiría siendo tan complicado acertar como cualquier concurrencia de memoria compartida de bajo nivel, pero no hay una razón fundamental por la que no funcione.

Sin embargo , tendría graves desventajas. Además de ocupar espacio adicional ( al menos dos palabras adicionales para todos los objetos), hace que cada desreferencia sea mucho más costosa. Incluso algo tan simple como obtener un atributo ahora implica una búsqueda completa de tabla hash. Yo estimaría que el golpe de rendimiento es mucho peor que para el rastreo incremental.


fuente
Así que tenemos una gran cantidad de memoria de hoy para que pudiéramos tener, digamos 50 Mb tabla de hash y podrían ser de módulo simple de modo que solamente una instrucción ...
mrpyo
3
@mrpyo obtiene el tamaño de la tabla hash, operación de módulo, desreferencia del desplazamiento de la tabla hash para obtener el puntero del objeto real, desreferencia al objeto en sí. Además, posiblemente, algunos registros barajando. Terminamos en 4+ instrucciones. Además, este esquema tiene problemas relacionados con la localidad de la memoria: ahora, tanto la tabla hash como los datos en sí tienen que encajar en la memoria caché.
amon
@mrpyo Necesita una entrada (ID de objeto -> dirección actual) por objeto, ¿verdad? E independientemente de cuán barata sea la función hash, tendrá colisiones y deberá resolverlas. También lo que dijo amon.
@amon es solo cuestión de tiempo antes de que las CPU tengan 50 MB o más de caché :)
Más el
1
@ Ӎσᶎ Para cuando podamos colocar 50 MiB de transistores en un chip y todavía tener una latencia lo suficientemente baja como para que funcione como caché L1 o L2 (los cachés L3 ya tienen un tamaño de hasta 15 MiB, pero generalmente AFAIK fuera del chip y lejos peor latencia que L1 y L2), tendremos en consecuencia grandes cantidades de memoria principal (y datos para poner en ella). La tabla no puede tener un tamaño fijo, debe crecer con el montón.
19

Todos los problemas en informática pueden resolverse con otro nivel de indirección ... excepto el problema de demasiadas capas de indirección

Su enfoque no resuelve de inmediato el problema de la recolección de basura, sino que solo lo sube un nivel. ¡Y a qué precio! Ahora, cada acceso a la memoria pasa por otra desreferencia de puntero. No podemos almacenar en caché la ubicación del resultado, ya que puede haber sido reubicada mientras tanto, siempre debemos pasar por la ID del objeto. En la mayoría de los sistemas, esta indirección no es aceptable, y se supone que detener el mundo tiene un costo total de tiempo de ejecución más bajo.

Dije que su propuesta solo mueve el problema, no lo resuelve. El problema está relacionado con la reutilización de ID de objetos. Las ID de objeto ahora son nuestro equivalente de punteros, y solo hay una cantidad finita de direcciones. Es concebible (especialmente en un sistema de 32 bits) que durante la vida útil de su programa, se hayan creado más de objetos INT_MAX, por ejemplo, en un bucle como

while (true) {
    Object garbage = new Object();
}

Si solo incrementamos la ID del objeto para cada objeto, nos quedaremos sin ID en algún momento. Por lo tanto, tenemos que averiguar qué ID todavía están en uso y cuáles son gratuitas para poder recuperarlas. ¿Suena familiar? Ahora estamos de vuelta en el punto de partida.

amon
fuente
Presumiblemente, ¿se pueden usar identificadores que sean 'lo suficientemente grandes' como digamos bignums de 256 bits? No digo que esta idea sea buena en general, pero es casi seguro que puedes reutilizar IDS.
Vality
@Vality de manera realista, sí, hasta donde podemos ver, eso solucionaría el problema de la reutilización de ID. Pero este es solo otro argumento de "640K debería ser suficiente para cualquiera", y en realidad no resuelve el problema. Un aspecto más catastrófico es que el tamaño de todos los objetos (y la tabla hash) tendría que aumentar para acomodar estos pseudo-punteros de gran tamaño, y que durante el acceso hash necesitamos comparar este bigint con otras ID que probablemente acapararán múltiples registros y tome varias instrucciones para completar (en 64 bits: 8 × carga, 4 × compare, 3 × y que es un aumento de 5 × sobre las entradas nativas).
amon
Sí, se quedaría sin ID después de un tiempo y necesitaría cambiarlos todos, lo que requeriría una pausa. Pero posiblemente sería un evento raro ...
mrpyo
@amon Muy de acuerdo, todos muy buenos puntos allí, es mucho mejor tener un sistema genuinamente sostenible, estoy de acuerdo. Esto va a ser insoportablemente lento, sea lo que sea que hagas, de todos modos solo es interesante en teoría. Sin embargo, personalmente no soy un gran fanático del recolector de basura: P
Vality
@amon: hay más código en el mundo que solo esto que saldría mal una vez que envuelva una ID de 64 bits (584 años de nanosegundos, y probablemente pueda organizar la asignación de memoria para tomar 1ns, especialmente si no fragmenta el contador global que escupe las identificaciones!). Pero claro, si no necesita confiar en eso, entonces no lo hace.
Steve Jessop
12

No hay ningún error en su línea de pensamiento, acaba de describir algo muy parecido al funcionamiento del recolector de basura Java original.

La máquina virtual Java original [6] y algunas máquinas virtuales Smalltalk usan punteros indirectos, llamados controladores en [6], para referirse a los objetos. Los mangos permiten una fácil reubicación de objetos durante la recolección de basura ya que, con los mangos, solo hay un puntero directo a cada objeto: el que está en su mango. Todas las demás referencias al objeto indirectas a través del mango. En tales sistemas de memoria basados ​​en identificadores, aunque las direcciones de los objetos cambian durante la vida útil de los objetos y, por lo tanto, no se pueden usar para el hash, las direcciones de los identificadores permanecen constantes.

Hashing de espacio y tiempo de objetos recolectados con basura

En la implementación actual de Sun de la máquina virtual Java, una referencia a una instancia de clase es un puntero a un controlador que es en sí mismo un par de punteros: uno a una tabla que contiene los métodos del objeto y un puntero al objeto de clase que representa el tipo del objeto, y el otro a la memoria asignada desde el montón de Java para los datos del objeto.

La especificación de máquina virtual Java (1997)

Por lo tanto, funciona, se ha probado y su ineficiencia condujo al desarrollo de sistemas de barrido y marca generacionales.

Pete Kirkham
fuente
Sin embargo, ¿presumiblemente estos identificadores no eran claves en una tabla hash (como en la pregunta)? No es necesario, solo una estructura que contiene un puntero. Luego, los identificadores son todos del mismo tamaño, por lo que pueden asignarse desde un asignador de almacenamiento dinámico. Que por su naturaleza no necesita compactación interna ya que no se fragmenta. Puede llorar la incapacidad de los grandes bloques utilizados por ese asignador para ser reubicados. Que se puede resolver con otro nivel de indirección ;-)
Steve Jessop
@SteveJessop sí, no hubo una tabla hash en la implementación de gc, aunque el valor del identificador también fue el valor devuelto porObject.getHashCode()
Pete Kirkham