¿Por qué son tan populares los punteros inteligentes de conteo de referencias?

52

Como puedo ver, los punteros inteligentes se utilizan ampliamente en muchos proyectos C ++ del mundo real.

Aunque algún tipo de punteros inteligentes son obviamente beneficiosos para soportar RAII y transferencias de propiedad, también existe la tendencia de usar punteros compartidos por defecto , como una forma de "recolección de basura" , de modo que el programador no tenga que pensar tanto en la asignación. .

¿Por qué los punteros compartidos son más populares que integrar un recolector de basura adecuado como Boehm GC ? (¿O está de acuerdo en absoluto en que son más populares que los GC reales?)

Conozco dos ventajas de los GC convencionales sobre el conteo de referencias:

  • Los algoritmos de GC convencionales no tienen problemas con los ciclos de referencia .
  • El recuento de referencias es generalmente más lento que un GC adecuado.

¿Cuáles son las razones para usar punteros inteligentes de conteo de referencias?

Miklós Homolya
fuente
66
Solo agregaría un comentario de que este es un valor predeterminado incorrecto: en la mayoría de los casos, std::unique_ptres suficiente y, como tal, tiene una sobrecarga cero sobre punteros sin procesar en términos de rendimiento en tiempo de ejecución. Al usarlo en std::shared_ptrtodas partes, también oscurecería la semántica de propiedad, perdiendo uno de los principales beneficios de los punteros inteligentes además de la administración automática de recursos: una comprensión clara de la intención detrás del código.
Matt
2
Lo sentimos, pero la respuesta aceptada aquí es completamente incorrecta. El conteo de referencias tiene mayores gastos generales (un conteo en lugar de un bit de marca y un rendimiento de tiempo de ejecución más lento), tiempos de pausa ilimitados cuando disminuye la avalancha y no más complejo que, digamos, el semi-espacio de Cheney.
Jon Harrop

Respuestas:

57

Algunas ventajas del recuento de referencias sobre la recolección de basura:

  1. Gastos indirectos bajos. Los recolectores de basura pueden ser bastante intrusivos (por ejemplo, hacer que su programa se congele en momentos impredecibles mientras se procesa un ciclo de recolección de basura) y bastante intensivo en memoria (por ejemplo, la huella de memoria de su proceso crece innecesariamente a muchos megabytes antes de que la recolección de basura finalmente se active)

  2. Comportamiento más predecible. Con el recuento de referencias, tiene la garantía de que su objeto se liberará en el instante en que desaparezca la última referencia. Con la recolección de basura, por otro lado, su objeto se liberará "en algún momento", cuando el sistema lo encuentre. Para la RAM, esto no suele ser un gran problema en equipos de escritorio o servidores con poca carga, pero para otros recursos (por ejemplo, identificadores de archivos) a menudo es necesario cerrarlos lo antes posible para evitar posibles conflictos más adelante.

  3. Más simple El recuento de referencias se puede explicar en unos minutos e implementar en una o dos horas. Los recolectores de basura, especialmente aquellos con un rendimiento decente, son extremadamente complejos y no mucha gente los entiende.

  4. Estándar. C ++ incluye el recuento de referencias (vía shared_ptr) y amigos en el STL, lo que significa que la mayoría de los programadores de C ++ están familiarizados con él y la mayoría del código de C ++ funcionará con él. Sin embargo, no hay ningún recolector de basura C ++ estándar, lo que significa que debe elegir uno y esperar que funcione bien para su caso de uso, y si no lo hace, es su problema solucionarlo, no el idioma.

En cuanto a los supuestos inconvenientes del recuento de referencias: no detectar ciclos es un problema, pero uno con el que nunca me he encontrado personalmente en los últimos diez años al usar el recuento de referencias. La mayoría de las estructuras de datos son naturalmente acíclicas, y si se encuentra con una situación en la que necesita referencias cíclicas (por ejemplo, puntero principal en un nodo de árbol), puede usar un débil_ptr o un puntero C sin formato para la "dirección hacia atrás". Siempre que conozca el problema potencial cuando diseña sus estructuras de datos, no es un problema.

En cuanto al rendimiento, nunca he tenido un problema con el rendimiento del recuento de referencias. He tenido problemas con el rendimiento de la recolección de basura, en particular los congelamientos aleatorios en los que puede incurrir GC, a los que la única solución ("no asignar objetos") podría reformularse como "no usar GC" .

Jeremy Friesner
fuente
16
Las implementaciones ingeniosas de conteo de referencias generalmente obtienen un rendimiento mucho menor que los GC de producción (30–40%) a expensas de la latencia. La brecha se puede cerrar con optimizaciones como el uso de menos bits para el recuento y evitar el seguimiento de los objetos hasta que escapen; C ++ hace esto de forma natural si principalmente make_sharedcuando regresa. Aún así, la latencia tiende a ser el mayor problema en las aplicaciones en tiempo real, pero el rendimiento es más importante en general, por lo que el GC de rastreo se usa tanto. No sería tan rápido para hablar mal de ellos.
Jon Purdy
3
Quibble 'más simple': más simple en términos de la cantidad total de implementación requerida , sí, pero no más simple para el código que lo usa : compare decirle a alguien cómo usar RC ('haga esto al crear objetos y esto al destruirlos' ) sobre cómo (ingenuamente, lo que a menudo es suficiente) usar GC ('...').
AakashM
44
"Con el recuento de referencias, tiene la garantía de que su objeto se liberará en el instante en que desaparezca la última referencia". Ese es un error común. flyingfrogblog.blogspot.co.uk/2013/10/…
Jon Harrop
44
@ JonHarrop: Esa publicación de blog está terriblemente equivocada. También debe leer todos los comentarios, especialmente el último.
Deduplicador
3
@ JonHarrop: Sí, lo hay. No entiende que la vida útil es el alcance completo que se extiende hasta la llave de cierre. Y la optimización en F # que, según los comentarios, solo a veces funciona, está terminando la vida útil antes, si la variable no se usa nuevamente. Que naturalmente tiene sus propios peligros.
Deduplicador
26

Para obtener un buen rendimiento de un GC, el GC debe poder mover objetos en la memoria. En un lenguaje como C ++ donde puede interactuar directamente con ubicaciones de memoria, esto es prácticamente imposible. (Microsoft C ++ / CLR no cuenta porque introduce una nueva sintaxis para los punteros administrados por GC y, por lo tanto, es efectivamente un lenguaje diferente).

El Boehm GC, aunque es una idea ingeniosa, es en realidad lo peor de ambos mundos: necesita un malloc () que sea más lento que un buen GC, por lo que pierde el comportamiento determinista de asignación / desasignación sin el aumento de rendimiento correspondiente de un GC generacional . Además, es necesariamente conservador, por lo que no necesariamente recogerá toda su basura de todos modos.

Un GC bien ajustado puede ser una gran cosa. Pero en un lenguaje como C ++, las ganancias son mínimas y los costos a menudo simplemente no valen la pena.

Sin embargo, será interesante ver que a medida que C ++ 11 se vuelve más popular, si las lambdas y la semántica de captura comienzan a conducir a la comunidad C ++ hacia los mismos tipos de problemas de asignación y vida útil de los objetos que causaron que la comunidad Lisp inventara GC en el primer sitio.

Vea también mi respuesta a una pregunta relacionada en StackOverflow .

Daniel Pryden
fuente
66
RE el Boehm GC, ocasionalmente me he preguntado cuánto es personalmente responsable de la aversión tradicional al GC entre los programadores de C y C ++ simplemente proporcionando una mala primera impresión de la tecnología en general.
Leushenko
@Leushenko Bien dicho. Un buen ejemplo es esta pregunta, donde Boehm gc se llama un gc "adecuado", ignorando el hecho de que es lento y prácticamente garantiza que tenga fugas. Encontré esta pregunta mientras investigaba si alguien implementó el interruptor de ciclo estilo python para shared_ptr, lo que parece un objetivo valioso para una implementación de c ++.
user4815162342
4

Como puedo ver, los punteros inteligentes se utilizan ampliamente en muchos proyectos C ++ del mundo real.

Es cierto, pero objetivamente, la gran mayoría del código ahora está escrito en idiomas modernos con rastreadores de basura.

Aunque algún tipo de punteros inteligentes son obviamente beneficiosos para soportar RAII y transferencias de propiedad, también existe la tendencia de usar punteros compartidos por defecto, como una forma de "recolección de basura", para que el programador no tenga que pensar tanto en la asignación. .

Esa es una mala idea porque aún debe preocuparse por los ciclos.

¿Por qué los punteros compartidos son más populares que integrar un recolector de basura adecuado como Boehm GC? (¿O está de acuerdo en absoluto en que son más populares que los GC reales?)

Oh wow, hay muchas cosas mal con tu línea de pensamiento:

  1. El GC de Boehm no es un GC "adecuado" en ningún sentido de la palabra. Es realmente horrible Es conservador, por lo que tiene fugas y es ineficiente por diseño. Ver: http://flyingfrogblog.blogspot.co.uk/search/label/boehm

  2. Los punteros compartidos, objetivamente, no son tan populares como GC porque la gran mayoría de los desarrolladores están usando lenguajes GC'd ahora y no necesitan punteros compartidos. Basta con mirar a Java y Javascript en el mercado laboral en comparación con C ++.

  3. Parece que está restringiendo la consideración a C ++ porque, supongo, cree que GC es un problema tangencial. No lo es (la única forma de obtener un GC decente es diseñar el lenguaje y la VM desde el principio), por lo que está introduciendo un sesgo de selección. Las personas que realmente quieren una recolección de basura adecuada no se quedan con C ++.

¿Cuáles son las razones para usar punteros inteligentes de conteo de referencias?

Está restringido a C ++ pero desearía tener una administración de memoria automática.

Jon Harrop
fuente
77
Um, es una pregunta etiquetada como C ++ que habla sobre las características de C ++. Claramente, cualquier declaración general se refiere al código C ++, no a la totalidad de la programación. Sin embargo, la recolección de basura "objetivamente" puede estar en uso fuera del mundo de C ++, lo que en última instancia es irrelevante para la pregunta en cuestión.
Nicol Bolas
2
Su última línea es evidentemente incorrecta: está en C ++ y se alegra de no verse obligado a lidiar con GC y se demora la liberación de recursos. Hay una razón por la que a Apple no le gusta GC, y la directriz más importante para los idiomas GC'd es: No cree basura a menos que tenga grandes cantidades de recursos inactivos o no pueda evitarlo.
Deduplicador
3
@JonHarrop: Entonces, compare pequeños programas equivalentes con y sin GC, que no se seleccionan explícitamente para jugar en beneficio de ambos lados. ¿Cuál esperas que necesite más memoria?
Deduplicador
1
@Dupuplicator: Puedo imaginar programas que den cualquier resultado. El recuento de referencias superaría al GC de rastreo cuando el programa está diseñado para mantener la memoria de asignación de almacenamiento dinámico hasta que sobreviva al vivero (por ejemplo, una cola de listas) porque ese es un rendimiento patológico para un GC generacional y generaría la basura más flotante. El seguimiento de la recolección de basura requeriría menos memoria que el recuento de referencias basado en el alcance cuando hay muchos objetos pequeños y las vidas son cortas pero no son estáticamente bien conocidas, por lo que algo así como un programa lógico que usa estructuras de datos puramente funcionales.
Jon Harrop
3
@ JonHarrop: quise decir con GC (rastreo o lo que sea) y RAII si hablas C ++. Que incluye el recuento de referencias, pero solo donde es útil. O puede compararlo con un programa Swift.
Deduplicador
3

En MacOS X e iOS, y con los desarrolladores que usan Objective-C o Swift, el recuento de referencias es popular porque se maneja automáticamente, y el uso de recolección de basura ha disminuido considerablemente ya que Apple ya no lo admite (me han dicho que las aplicaciones usan la recolección de basura se interrumpirá en la próxima versión de MacOS X, y la recolección de basura nunca se implementó en iOS). De hecho, dudo seriamente de que haya mucho software utilizando recolección de basura cuando estaba disponible.

La razón para deshacerse de la recolección de basura: nunca funcionó de manera confiable en un entorno de estilo C donde los punteros podrían "escapar" a áreas no accesibles para el recolector de basura. Apple cree firmemente y cree que el conteo de referencias es más rápido. (Puede hacer cualquier reclamo aquí sobre la velocidad relativa, pero nadie ha podido convencer a Apple). Y al final, nadie usó la recolección de basura.

Lo primero que aprende cualquier desarrollador de MacOS X o iOS es cómo manejar los ciclos de referencia, por lo que no es un problema para un desarrollador real.

gnasher729
fuente
Según tengo entendido, no fue que se trata de un entorno tipo C que decidió las cosas, sino que GC es indeterminado y necesita mucha más memoria para tener un rendimiento aceptable, y un servidor / escritorio externo que siempre es un poco escaso.
Deduplicador
La depuración de la razón por el recolector de basura destruye un objeto que todavía estaba usando (que conduce a un accidente) decidió que para mí :-)
gnasher729
Oh sí, eso también lo haría. ¿Al final descubriste por qué?
Deduplicador
Sí, fue una de las muchas funciones de Unix en las que pasa un vacío * como "contexto" que luego se le devuelve en una función de devolución de llamada; el vacío * era realmente un objeto Objective-C, y el recolector de basura no se dio cuenta de que el objeto estaba escondido en la llamada de Unix. Se llama a la devolución de llamada, emite vacío * al Objeto *, ¡kaboom!
gnasher729
2

La mayor desventaja de la recolección de basura en C ++ es que es imposible hacerlo bien:

  • En C ++, los punteros no viven en su propia comunidad amurallada, se mezclan con otros datos. Como tal, no puede distinguir un puntero de otros datos que simplemente tienen un patrón de bits que puede interpretarse como un puntero válido.

    Consecuencia: Cualquier recolector de basura C ++ filtrará objetos que deberían ser recolectados.

  • En C ++, puede hacer aritmética de puntero para derivar punteros. Como tal, si no encuentra un puntero al comienzo de un bloque, eso no significa que no se pueda hacer referencia a ese bloque.

    Consecuencia: cualquier recolector de basura C ++ tiene que tener en cuenta estos ajustes, tratando cualquier secuencia de bits que apunte a cualquier lugar dentro de un bloque, incluso justo después del final, como un puntero válido que hace referencia al bloque.

    Nota: Ningún recolector de basura C ++ puede manejar código con trucos como estos:

    int* array = new int[7];
    array--;    //undefined behavior, but people may be tempted anyway...
    for(int i = 1; i <= 7; i++) array[i] = i;
    

    Es cierto que esto invoca un comportamiento indefinido. Pero parte del código existente es más inteligente de lo que es bueno para él, y puede provocar la desasignación preliminar por parte de un recolector de basura.

cmaster
fuente
2
" están mezclados con otros datos " . No es tanto que estén "mezclados" con otros datos. Es fácil usar el sistema de tipo C ++ para ver qué es un puntero y qué no. El problema es que los punteros con frecuencia se convierten en otros datos. Ocultar un puntero en un entero es una herramienta desafortunadamente común para muchas API de estilo C.
Nicol Bolas
1
Ni siquiera necesita un comportamiento indefinido para arruinar un recolector de basura en c ++. Podría, por ejemplo, serializar un puntero a un archivo y leerlo más tarde. Mientras tanto, es posible que su proceso no contenga ese puntero en ningún lugar de su espacio de direcciones, por lo que el recolector de basura podría recolectar ese objeto, y luego cuando deserialice el puntero ... Vaya.
Bwmat
@Bwmat "Even"? Escribir punteros en un archivo como ese parece un poco ... descabellado. De todos modos, el mismo problema serio afecta a los punteros para apilar objetos, ¡pueden desaparecer cuando lees el puntero desde el archivo en otra parte del código! Deserializar el valor de puntero no válido es un comportamiento indefinido, no lo hagas.
hyde
Por supuesto, deberías tener cuidado si estás haciendo algo así. Se suponía que era un ejemplo de que, en general, un recolector de basura no puede funcionar 'correctamente' en todos los casos en c ++ (sin cambiar el idioma)
Bwmat
1
@ gnasher729: Ehm, ¿no? ¿Los punteros del pasado están perfectamente bien?
Deduplicador