Pregunta

Sigo escuchando a la gente quejarse de que C ++ no tiene recolección de basura. También escuché que el Comité de Normas de C ++ está considerando agregarlo al lenguaje. Me temo que simplemente no veo el punto ... usar RAII con punteros inteligentes elimina la necesidad de hacerlo, ¿no?

Mi única experiencia con la recolección de basura fue en un par de computadoras personales baratas de los años ochenta, lo que significaba que el sistema se congelaría por unos segundos cada cierto tiempo. Estoy seguro de que ha mejorado desde entonces, pero como puedes adivinar, eso no me dejó con una buena opinión al respecto.

¿Qué ventajas podría ofrecer la recolección de basura a un desarrollador de C ++ experimentado?

¿Fue útil?

Solución

Sigo escuchando a personas que se quejan de que C ++ no tiene recolección de basura.

Lo siento mucho por ellos. En serio.

C ++ tiene RAII, y siempre me quejo de no encontrar RAII (o un RAII castrado) en los idiomas de recolección de basura.

¿Qué ventajas podría ofrecer la recolección de basura a un desarrollador de C ++ con experiencia?

Otra herramienta.

Matt J lo escribió muy bien en su publicación ( recolección de basura en C ++: por qué ? ): No necesitamos las funciones de C ++, ya que la mayoría de ellas se pueden codificar en C, y no necesitamos las funciones de C, ya que la mayoría de ellas se pueden codificar en Ensamblado, etc. C ++ debe evolucionar .

Como desarrollador: no me importa GC. Probé RAII y GC, y creo que RAII es muy superior. Como lo dijo Greg Rogers en su publicación ( recolección de basura en C ++, ¿por qué? ), las fugas de memoria no son tan terribles (al menos en C ++, donde son raras si realmente se usa C ++) para justificar GC en lugar de RAII. GC tiene una asignación / finalización no determinista y es solo una forma de escribir un código que no le importa con las opciones de memoria específicas .

Esta última oración es importante: es importante escribir el código que "a juste no le importa". De la misma manera, en C ++ RAII no nos importa la liberación de recursos porque RAII lo hace por nosotros, o la inicialización de objetos porque el constructor lo hace por nosotros, a veces es importante simplemente codificar sin importar quién es el propietario de qué memoria, y qué tipo de puntero (compartido, débil, etc.) necesitamos para este o este fragmento de código. Parece que hay una necesidad de GC en C ++. (incluso si personalmente no lo veo)

Un ejemplo de buen uso de GC en C ++

A veces, en una aplicación, tienes " datos flotantes " ;. Imagine una estructura de datos similar a un árbol, pero nadie es realmente " propietario " de los datos (y a nadie realmente le importa cuándo será destruido exactamente). Múltiples objetos pueden usarlo, y luego, descartarlo. Desea que se libere cuando ya nadie lo esté utilizando.

El enfoque de C ++ está utilizando un puntero inteligente. El impulso :: shared_ptr viene a la mente. Por lo tanto, cada dato es propiedad de su propio puntero compartido. Guay. El problema es que cuando cada dato puede referirse a otro dato. No puede usar punteros compartidos porque están usando un contador de referencia, que no admite referencias circulares (A apunta a B y B apunta a A). Así que debes saber mucho sobre dónde usar los punteros débiles (boost :: weak_ptr) y cuándo usar los punteros compartidos.

Con un GC, solo usas los datos estructurados del árbol.

El inconveniente es que no debe importarle cuando los " datos flotantes " realmente será destruido. Solo que será destruido.

Conclusión

Al final, si se hace correctamente y es compatible con los modismos actuales de C ++, GC sería una Otra herramienta buena para C ++ .

C ++ es un lenguaje multiparadigm: agregar un GC quizás haga que algunos fanáticos de C ++ lloren por traición, pero al final, podría ser una buena idea, y creo que el Comité de Normas de C ++ no permitirá que este tipo de mayor rompa el lenguaje, por lo que podemos confiar en que hagan el trabajo necesario para habilitar un GC C ++ correcto que no interfiera con C ++: Como siempre en C ++, si no necesita una función, no la use y no te costará nada.

Otros consejos

La respuesta corta es que la recolección de basura es muy similar en principio a RAII con punteros inteligentes. Si cada parte de la memoria que alguna vez asigna se encuentra dentro de un objeto, y los punteros inteligentes hacen referencia a ese objeto, tiene algo cerca de la recolección de basura (potencialmente mejor). La ventaja proviene de no tener que ser tan juiciosos con respecto al alcance y el puntero inteligente de cada objeto, y dejar que el tiempo de ejecución haga el trabajo por usted.

Esta pregunta parece análoga a " ¿qué tiene C ++ para ofrecer al desarrollador de ensamblajes con experiencia? Las instrucciones y las subrutinas eliminan la necesidad, ¿no? "

Con el advenimiento de buenos revisores de memoria como valgrind, no veo mucho uso para la recolección de basura como una red de seguridad " en el caso " nos olvidamos de desasignar algo, especialmente porque no ayuda mucho en la gestión del caso más genérico de recursos distintos de la memoria (aunque estos son mucho menos comunes). Además, la asignación explícita y la desasignación de la memoria (incluso con punteros inteligentes) es bastante rara en el código que he visto, ya que los contenedores suelen ser mucho más simples y mejores.

Pero la recolección de basura puede ofrecer beneficios de rendimiento potencialmente, especialmente si se asigna un montón de objetos de corta duración. GC también ofrece potencialmente una mejor localidad de referencia para los objetos recién creados (comparables a los objetos en la pila).

El factor motivador para el soporte de GC en C ++ parece ser la programación lambda, las funciones anónimas, etc. Resulta que las bibliotecas lambda se benefician de la capacidad de asignar memoria sin preocuparse por la limpieza. El beneficio para los desarrolladores normales sería más simple, más confiable y más rápido compilando las bibliotecas lambda.

GC también ayuda a simular la memoria infinita; La única razón por la que necesita eliminar POD es que necesita reciclar la memoria. Si tiene GC o memoria infinita, ya no es necesario eliminar los POD.

El comité no está agregando recolección de basura, está agregando un par de características que permiten que la recolección de basura sea implementada de manera más segura. Solo el tiempo dirá si realmente tienen algún efecto en los compiladores futuros. Las implementaciones específicas podrían variar ampliamente, pero lo más probable es que impliquen una recopilación basada en la accesibilidad, lo que podría implicar un pequeño bloqueo, dependiendo de cómo se realice.

Una cosa es, sin embargo, ningún recolector de basura que cumpla con los estándares podrá llamar a los destructores, solo para reutilizar silenciosamente la memoria perdida.

¿Qué ventajas podría ofrecer la recolección de basura a un desarrollador de C ++ con experiencia?

No tener que buscar las fugas de recursos en el código de sus colegas con menos experiencia.

No entiendo cómo se puede argumentar que RAII reemplaza a GC o es muy superior. Hay muchos casos manejados por un CG que RAII simplemente no puede manejar en absoluto. Son bestias diferentes.

Primero, RAII no es una prueba de balas: funciona contra algunas fallas comunes que son generalizadas en C ++, pero hay muchos casos en que RAII no ayuda en absoluto; es frágil a los eventos asíncronos (como las señales en UNIX). Fundamentalmente, RAII se basa en el alcance: cuando una variable está fuera de alcance, se libera automáticamente (suponiendo que el destructor se haya implementado correctamente, por supuesto).

Este es un ejemplo sencillo en el que ni auto_ptr ni RAII pueden ayudarlo:

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#include <memory>

using namespace std;

volatile sig_atomic_t got_sigint = 0;

class A {
        public:
                A() { printf("ctor\n"); };
                ~A() { printf("dtor\n"); };
};

void catch_sigint (int sig)
{
        got_sigint = 1;
}

/* Emulate expensive computation */
void do_something()
{
        sleep(3);
}

void handle_sigint()
{
        printf("Caught SIGINT\n");
        exit(EXIT_FAILURE);
}

int main (void)
{
        A a;
        auto_ptr<A> aa(new A);

        signal(SIGINT, catch_sigint);

        while (1) {
                if (got_sigint == 0) {
                        do_something();
                } else {
                        handle_sigint();
                        return -1;
                }
        }
}

El destructor de A nunca será llamado. Por supuesto, es un ejemplo artificial y algo artificial, pero en realidad puede ocurrir una situación similar; por ejemplo, cuando su código es llamado por otro código que maneja SIGINT y sobre el cual usted no tiene ningún control (ejemplo concreto: extensiones mex en matlab). Es la misma razón por la que finalmente en Python no garantiza la ejecución de algo. Gc puede ayudarte en este caso.

Otros modismos no juegan bien con esto: en cualquier programa no trivial, necesitarás objetos con estado (estoy usando la palabra objeto en un sentido muy amplio aquí, puede ser cualquier construcción permitida por el lenguaje); Si necesita controlar el estado fuera de una función, no puede hacerlo fácilmente con RAII (por lo que RAII no es tan útil para la programación asíncrona). OTOH, gc tiene una vista de toda la memoria de su proceso, es decir, conoce todos los objetos que asignó y puede limpiar de forma asíncrona.

También puede ser mucho más rápido usar gc, por los mismos motivos: si necesita asignar / desasignar muchos objetos (en particular, objetos pequeños), gc superará ampliamente a RAII, a menos que escriba un asignador personalizado, ya que gc Puede asignar / limpiar muchos objetos en una sola pasada. Algunos proyectos conocidos de C ++ utilizan gc, incluso cuando el rendimiento es importante (vea, por ejemplo, Tim Sweenie sobre el uso de gc en Unreal Tournament: http://lambda-the-ultimate.org/node/1277 ). GC básicamente aumenta el rendimiento a costa de la latencia.

Por supuesto, hay casos en que RAII es mejor que gc; en particular, el concepto gc se refiere principalmente a la memoria, y ese no es el único recurso. Cosas como el archivo, etc ... se pueden manejar bien con RAII. Los idiomas sin manejo de memoria como python o ruby ??tienen algo como RAII para esos casos, BTW (con una declaración en python). RAII es muy útil cuando necesitas controlar con precisión cuándo se libera el recurso, y ese es el caso más frecuente de los archivos o bloqueos, por ejemplo.

Es un error general asumir que, dado que C ++ no tiene la recolección de basura integrada en el idioma , no puede usar la recolección de basura en el período de C ++. Esto no tiene sentido. Sé de programadores de élite de C ++ que usan el coleccionista Boehm como una cuestión de rutina en su trabajo.

La recolección de basura permite posponer la decisión sobre quién posee un objeto.

C ++ usa semántica de valor, por lo que con RAII, de hecho, los objetos se recolectan cuando se salen del alcance. Esto a veces se denomina GC inmediato.

Cuando su programa comienza a usar la semántica de referencia (a través de punteros inteligentes, etc.), el lenguaje ya no lo respalda, quedará al tanto de su biblioteca de punteros inteligentes.

Lo complicado de GC es decidir cuándo cuando ya no se necesita un objeto.

La recolección de basura hace que la RCU sea más fácil de sincronizar sin problemas.

Mayor seguridad y escalabilidad de los hilos

Hay una propiedad de GC que puede ser muy importante en algunos escenarios. La asignación del puntero es naturalmente atómica en la mayoría de las plataformas, mientras que la creación de puntos de referencia contabilizados ("smart") es bastante difícil e introduce una sobrecarga importante de sincronización. Como resultado, los punteros inteligentes a menudo se dicen "no escalar bien" en arquitectura multi-core.

La recolección de basura es realmente la base para la administración automática de recursos. Y tener GC cambia la forma en que aborda los problemas de una manera que es difícil de cuantificar. Por ejemplo, cuando está realizando la administración manual de recursos, necesita:

  • Considere cuándo se puede liberar un artículo (¿todos los módulos / clases han terminado con él?)
  • Considere quién es responsable de liberar un recurso cuando esté listo para ser liberado (¿qué clase / módulo debería liberar este artículo?)

En el caso trivial no hay complejidad. P.ej. abre un archivo al comienzo de un método y lo cierra al final. O la persona que llama debe liberar este bloque de memoria devuelto.

Las cosas comienzan a complicarse rápidamente cuando tiene varios módulos que interactúan con un recurso y no está tan claro quién necesita limpiar. El resultado final es que todo el enfoque para abordar un problema incluye ciertos patrones de programación y diseño que son un compromiso.

En los idiomas que tienen recolección de basura, puede usar un desechable


Los punteros inteligentes, que en realidad son un ejemplo perfecto de los compromisos que mencioné. Los punteros inteligentes no pueden evitar que pierda estructuras de datos cíclicos a menos que tenga un mecanismo de copia de seguridad. Para evitar este problema, a menudo se compromete y evita el uso de una estructura cíclica, aunque podría ser la mejor opción.

Yo también tengo dudas de que el comité de C ++ está agregando una colección de basura completa al estándar.

Pero diría que la razón principal para agregar / tener recolección de basura en el lenguaje moderno es que hay muy pocas razones buenas contra la recolección de basura. Desde los años ochenta hubo varios avances enormes en el campo de la gestión de la memoria y la recolección de basura, y creo que incluso existen estrategias de recolección de basura que podrían ofrecerle garantías en tiempo real (como que, "GC no va a tomar más que". ... en el peor de los casos ").

  

el uso de RAII con punteros inteligentes elimina la necesidad de hacerlo, ¿no?

Los punteros inteligentes se pueden usar para implementar el recuento de referencias en C ++, que es una forma de recolección de basura (gestión de memoria automática), pero los GC de producción ya no usan el recuento de referencias porque tiene algunas deficiencias importantes:

  1. Ciclos de fugas de conteo de referencia. Considere A & # 8596; B, ambos objetos A y B se refieren entre sí, por lo que ambos tienen un recuento de referencia de 1 y ninguno de ellos se recopila, pero ambos deben recuperarse. Los algoritmos avanzados como eliminación de prueba resuelven este problema pero agregan mucho de complejidad. El uso de weak_ptr como solución alternativa está en la administración de memoria manual.

  2. El conteo de referencias ingenuas es lento por varias razones. En primer lugar, requiere que los recuentos de referencias fuera de la caché sean superados a menudo (consulte Boost's shared_ptr hasta 10 & # 215; más lento que la recolección de basura de OCaml ). En segundo lugar, los destructores inyectados al final del alcance pueden incurrir en llamadas a funciones virtuales innecesarias y costosas e inhibir optimizaciones como la eliminación de llamadas de cola.

  3. El recuento de referencias basado en el alcance mantiene la basura flotante alrededor ya que los objetos no se reciclan hasta el final del alcance, mientras que el seguimiento de los GC puede recuperarlos tan pronto como sean inaccesibles, por ejemplo. ¿Se puede asignar un local asignado antes de reclamar un bucle durante el bucle?

  

¿Qué ventajas podría ofrecer la recolección de basura a un desarrollador de C ++ experimentado?

La productividad y la fiabilidad son los principales beneficios. Para muchas aplicaciones, la administración de memoria manual requiere un esfuerzo significativo del programador. Al simular una máquina de memoria infinita, la recolección de basura libera al programador de esta carga que les permite centrarse en la resolución de problemas y evade algunas clases importantes de errores (punteros colgantes, falta free , doble free ). Además, la recolección de basura facilita otras formas de programación, p. Ej. resolviendo el upwards funarg problem (1970) .

En un marco que admite GC, una referencia a un objeto inmutable, como una cadena, se puede pasar de la misma manera que una primitiva. Considere la clase (C # o Java):

public class MaximumItemFinder
{
  String maxItemName = "";
  int maxItemValue = -2147483647 - 1;

  public void AddAnother(int itemValue, String itemName)
  {
    if (itemValue >= maxItemValue)
    {
      maxItemValue = itemValue;
      maxItemName = itemName;
    }
  }
  public String getMaxItemName() { return maxItemName; }
  public int getMaxItemValue() { return maxItemValue; }
}

Tenga en cuenta que este código nunca tiene que hacer nada con el contenido de ninguna de las cadenas, y puede simplemente tratarlas como primitivas. Una declaración como maxItemName = itemName; generará dos instrucciones: una carga de registro seguida de un almacén de registros. El MaximumItemFinder no tendrá forma de saber si las personas que llaman a AddAnother van a conservar alguna referencia a las cadenas pasadas, y las personas que llaman no tendrán forma de saber cuánto tiempo < code> MaximumItemFinder mantendrá las referencias a ellos. Quienes llamen a getMaxItemName no tendrán forma de saber si y cuando MaximumItemFinder y el proveedor original de la cadena devuelta han abandonado todas las referencias a la misma. Debido a que el código simplemente puede pasar las referencias de cadena alrededor de valores primitivos similares, sin embargo, ninguna de esas cosas importa .

Tenga en cuenta también que si bien la clase anterior no sería segura para subprocesos en presencia de llamadas simultáneas a AddAnother , cualquier llamada a GetMaxItemName se garantizará devuelva una referencia válida a una cadena vacía o a una de las cadenas que se han pasado a AddAnother . La sincronización de subprocesos sería necesaria si uno quisiera asegurar cualquier relación entre el nombre del elemento máximo y su valor, pero la seguridad de la memoria está garantizada incluso en su ausencia .

No creo que haya ninguna forma de escribir un método como el anterior en C ++ que pueda mantener la seguridad de la memoria en presencia de un uso multihebra arbitrario sin usar la sincronización de subprocesos o de lo contrario requerir que cada variable de cadena tenga su propia copia de su contenido, mantenido en su propio espacio de almacenamiento, que no puede ser liberado o reubicado durante la vida útil de la variable en cuestión. Ciertamente, no sería posible definir un tipo de referencia de cadena que pueda definirse, asignarse y darse a conocer de manera tan económica como un int .

La recolección de basura puede hacer que las fugas sean tu peor pesadilla

El GC completo que maneja cosas como referencias cíclicas sería algo así como una actualización sobre un shared_ptr . De alguna manera lo agradecería en C ++, pero no en el nivel del idioma.

Una de las bellezas sobre C ++ es que no te obliga a recolectar basura.

Quiero corregir un error común: un mito de recolección de basura que de alguna manera elimina las fugas. Desde mi experiencia, las peores pesadillas de depurar el código escrito por otros y tratar de detectar las fugas lógicas más costosas involucraron la recolección de basura con lenguajes como Python incrustado a través de una aplicación host de uso intensivo de recursos.

Al hablar de temas como GC, hay teoría y luego práctica. En teoría es maravilloso y evita fugas. Sin embargo, a nivel teórico, todos los idiomas son maravillosos y están libres de fugas, ya que en teoría, todos escribirían el código perfectamente correcto y probarían cada caso posible en el que una sola pieza de código podría salir mal.

La recolección de basura combinada con una colaboración en equipo menos que ideal provocó las peores fugas más difíciles de depurar en nuestro caso.

El problema todavía tiene que ver con la propiedad de los recursos. Debe tomar decisiones de diseño claras cuando se trata de objetos persistentes, y la recolección de basura hace que sea muy fácil pensar que no.

Dado algún recurso, R , en un entorno de equipo en el que los desarrolladores no se comunican y revisan el código de los demás constantemente en todo momento (algo un poco demasiado común en mi experiencia), se convierte en bastante fácil para el desarrollador A almacenar un identificador para ese recurso. El desarrollador B también lo hace, quizás de una manera oscura que indirectamente agrega R a alguna estructura de datos. Lo mismo ocurre con C . En un sistema de recolección de basura, esto ha creado 3 propietarios de R .

Debido a que el desarrollador A fue el que creó el recurso originalmente y cree que es su dueño, recuerda que debe liberar la referencia a R cuando el usuario indica que Ya no quiere usarlo. Después de todo, si no lo hace, no pasaría nada y sería obvio al probar que la lógica de eliminación del usuario no hizo nada. Así que recuerda lanzarlo, como haría cualquier desarrollador razonablemente competente. Esto activa un evento para el cual B lo maneja y también recuerda liberar la referencia a R .

Sin embargo, C se olvida. No es uno de los desarrolladores más fuertes del equipo: un recluta algo nuevo que solo ha trabajado en el sistema durante un año. O tal vez ni siquiera está en el equipo, solo un popular desarrollador externo que escribe complementos para nuestro producto que muchos usuarios agregan al software. Con la recolección de basura, esto es cuando obtenemos esas fugas de recursos lógicos silenciosos. Son de la peor clase: no se manifiestan necesariamente en el lado visible del software del software como un error obvio, además del hecho de que durante el tiempo que se ejecuta el programa, el uso de la memoria sigue aumentando y aumentando con algún propósito misterioso. Tratar de reducir estos problemas con un depurador puede ser tan divertido como depurar una condición de carrera sensible al tiempo.

Sin la recolección de basura, el desarrollador C habría creado un puntero colgante . Puede intentar acceder a él en algún momento y hacer que el software se bloquee. Ahora que es un error de prueba / visible por el usuario. C se avergüenza un poco y corrige su error. En el escenario de GC, solo tratar de averiguar dónde está perdiendo el sistema puede ser tan difícil que algunas de las filtraciones nunca se corrijan. Estas no son fugas físicas de tipo valgrind que pueden detectarse fácilmente y ubicarse en una línea de código específica.

Con la recolección de basura, el desarrollador C ha creado una fuga muy misteriosa. Su código puede continuar accediendo a R , que ahora es solo una entidad invisible en el software, es irrelevante para el usuario en este punto, pero aún se encuentra en un estado válido. Y a medida que el código de C crea más fugas, crea más procesamiento oculto en recursos irrelevantes, y el software no solo pierde memoria, sino que también se vuelve cada vez más lento.

Por lo tanto, la recolección de basura no necesariamente mitiga las fugas de recursos lógicos. Puede, en escenarios menos que ideales, hacer que las fugas sean mucho más fáciles de pasar inadvertidamente y permanecer en el software. Los desarrolladores pueden sentirse tan frustrados al intentar rastrear sus pérdidas lógicas de GC que simplemente les dicen a sus usuarios que reinicien el software periódicamente como solución alternativa. Elimina los punteros que cuelgan, y en un software obsesionado con la seguridad donde el bloqueo es completamente inaceptable en cualquier situación, entonces preferiría GC. Pero a menudo trabajo en productos menos críticos para la seguridad pero que requieren un uso intensivo de los recursos, en los que es preferible una falla que se pueda solucionar de manera inmediata a un error silencioso muy misterioso y oscuro, y las fugas de recursos no son errores triviales allí. p>

En ambos casos, estamos hablando de objetos persistentes que no residen en la pila, como un gráfico de escena en un software 3D o los clips de video disponibles en un compositor o los enemigos en un mundo de juego. Cuando los recursos vinculan sus vidas a la pila, tanto C ++ como cualquier otro lenguaje de GC tienden a hacer que la administración de los recursos sea trivial. La verdadera dificultad radica en los recursos persistentes que hacen referencia a otros recursos.

En C o C ++, puede tener punteros colgantes y bloqueos resultantes de segfaults si no puede designar claramente quién posee un recurso y cuándo deben liberarse los identificadores (por ejemplo, establecerlo en nulo en respuesta a un evento). Sin embargo, en GC, ese choque fuerte y desagradable, pero a menudo fácil de detectar, se intercambia por una fuga silenciosa de recursos que tal vez nunca se detecte.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top