Pregunta RAII contra el recolector de basura


Hace poco vi una gran charla de Herb Sutter sobre "Leak Free C ++ ..." en CppCon 2016 donde habló sobre el uso de punteros inteligentes para implementar RAII (adquisición de recursos es inicialización) Conceptos y cómo resuelven la mayoría de los problemas de pérdida de memoria.

Ahora me estaba preguntando. Si sigo estrictamente las reglas de RAII, lo que parece ser algo bueno, ¿por qué sería eso diferente de tener un recolector de basura en C ++? Sé que con RAII el programador tiene el control total de cuándo se liberan los recursos nuevamente, pero ¿es eso en cualquier caso beneficioso para simplemente tener un recolector de basura? ¿Sería realmente menos eficiente? Incluso me enteré de que tener un recolector de basura puede ser más eficiente, ya que puede liberar grandes trozos de memoria a la vez en lugar de liberar pequeñas piezas de memoria en todo el código.


73
2018-06-02 09:12


origen


Respuestas:


Si sigo estrictamente las reglas de RAII, lo que parece ser algo bueno, ¿por qué sería eso diferente de tener un recolector de basura en C ++?

Si bien ambos se ocupan de las asignaciones, lo hacen de maneras completamente diferentes. Si está refiriendo a un GC como el de Java, eso agrega su propia sobrecarga, elimina parte del determinismo del proceso de liberación de recursos y maneja las referencias circulares.

Sin embargo, puede implementar GC para casos particulares, con características de rendimiento muy diferentes. Implementé una vez para cerrar las conexiones de socket, en un servidor de alto rendimiento / alto rendimiento (simplemente llamar al socket cerrar API tomó demasiado tiempo y borked el rendimiento del rendimiento). Esto no implicaba memoria, sino conexiones de red, y ningún manejo de dependencia cíclica.

Sé que con RAII el programador tiene el control total de cuándo se liberan los recursos nuevamente, pero ¿es eso en cualquier caso beneficioso para simplemente tener un recolector de basura?

Este determinismo es una característica que GC simplemente no permite. A veces tu querer para poder saber que después de cierto punto, se ha realizado una operación de limpieza (eliminación de un archivo temporal, cierre de una conexión de red, etc.).

En tales casos, GC no lo corta, que es la razón en C # (por ejemplo) que tiene el IDisposable interfaz.

Incluso me enteré de que tener un recolector de basura puede ser más eficiente, ya que puede liberar grandes trozos de memoria a la vez en lugar de liberar pequeñas piezas de memoria en todo el código.

Puede ser ... depende de la implementación.


61
2018-06-02 09:37



La recolección de basura resuelve ciertas clases de problemas de recursos que RAII no puede resolver. Básicamente, se reduce a dependencias circulares donde no se identifica el ciclo antes de la mano.

Esto le da dos ventajas. En primer lugar, habrá ciertos tipos de problemas que RAII no puede resolver. Estos son, en mi experiencia, raros.

El más grande es que le permite al programador ser flojo y no importa acerca de los tiempos de vida de los recursos de memoria y de otros recursos que no le molesta la demora en la limpieza. Cuando no tiene que preocuparse por ciertos tipos de problemas, puede interesarse Más sobre otros problemas Esto le permite enfocarse en las partes de su problema en las que desea enfocarse.

La desventaja es que sin RAII, administrar los recursos cuya vida desea restringir es difícil. Los lenguajes de GC básicamente lo reducen a tener tiempos de vida extremadamente simples limitados por el alcance o requieren que haga la administración de recursos manualmente, como en C, al indicar manualmente que ha terminado con un recurso. Su sistema de vida de objetos está fuertemente ligado a GC, y no funciona bien para una gestión estrecha de por vida de sistemas complejos grandes (pero sin ciclos).

Para ser justos, la administración de recursos en C ++ requiere mucho trabajo para funcionar correctamente en sistemas tan grandes (pero sin ciclos). C # y otros idiomas similares lo hacen un poco más difícil, a cambio facilitan la tarea fácil.

La mayoría de las implementaciones de GC también fuerzan clases completas de no localidad; crear búferes contiguos de objetos generales, o componer objetos generales en un objeto más grande, no es algo que la mayoría de las implementaciones de GC faciliten. Por otro lado, C # le permite crear un tipo de valor structs con capacidades algo limitadas. En la era actual de la arquitectura de CPU, la compatibilidad con la memoria caché es clave, y la falta de fuerzas GC locales es una carga pesada. Como estos lenguajes tienen un tiempo de ejecución de bytecode en su mayor parte, en teoría el entorno JIT podría mover datos comúnmente usados ​​juntos, pero la mayoría de las veces solo se obtiene una pérdida de rendimiento uniforme debido a fallas frecuentes de caché en comparación con C ++.

El último problema con GC es que la desasignación es indeterminada y, a veces, puede causar problemas de rendimiento. Los GC modernos hacen de este un problema menor de lo que ha sido en el pasado.


38
2018-06-02 12:34



Darse cuenta de RAII es un modismo de programación, mientras GC es una técnica de gestión de memoria. Entonces, estamos comparando manzanas con naranjas.

Pero podemos restringir RAII a sus aspectos de gestión de memoria solamente y compara eso con las técnicas de GC.

La principal diferencia entre las llamadas técnicas de administración de memoria basadas en RAII (lo que realmente significa recuento de referencia, al menos cuando considera recursos de memoria e ignora los otros, como archivos) y genuino recolección de basura técnicas es el manejo de circular referencias (para gráficos cíclicos)

Con el recuento de referencias, debe codificar especialmente para ellos (utilizando referencias débiles u otras cosas).

En muchos casos útiles (piense en std::vector<std::map<std::string,int>>) el conteo de referencia es implícito (ya que solo puede ser 0 o 1) y prácticamente se omite, pero las funciones contructor y destructor (esenciales para RAII) se comportan como si hubiera un bit de conteo de referencia (que prácticamente no existe). En std::shared_ptr hay un contador de referencia genuino. Pero la memoria todavía está implícitamente  administrado manualmente (con new y delete desencadenado dentro de constructores y destructores), pero ese "implícito" delete(en destructores) da la ilusión de la administración automática de la memoria. Sin embargo, llamadas a new y delete todavía suceden (y cuestan tiempo).

Por cierto, el GC implementación puede (y con frecuencia lo hace) manejar la circularidad de alguna manera especial, pero deja esa carga al GC (por ejemplo, lea sobre el Algoritmo de Cheney)

Algunos algoritmos de GC (especialmente el recolector de basura de copia generacional) no se molestan en liberar memoria para individual objetos, es lanzamiento en masse después de la copia. En la práctica, el Ocaml GC (o el SBCL uno) puede ser más rápido que un genuino estilo de programación C ++ RAII (para algunos, no todo tipo de algoritmos).

Algunos GC proporcionan finalización (mayormente usado para administrar sin memoria recursos externos como archivos), pero rara vez lo usará (ya que la mayoría de los valores solo consumen recursos de memoria). La desventaja es que la finalización no ofrece ninguna garantía de tiempo. Hablando en términos prácticos, un programa que utiliza la finalización lo está utilizando como último recurso (por ejemplo, el cierre de archivos debería ocurrir más o menos explícitamente fuera de la finalización, y también con ellos).

Aún puede tener pérdidas de memoria con GC (y también con RAII, al menos si no se usa correctamente), p. cuando un valor se mantiene en alguna variable o campo, pero nunca se usará en el futuro. Simplemente suceden con menos frecuencia.

Recomiendo leer el manual de recolección de basura.

En su código C ++, puede usar GC de Boehm o MPS de Ravenbrook o codifica tu propio rastreo de recolector de basura. Por supuesto, usar un GC es una compensación (hay algunos inconvenientes, por ejemplo, no determinismo, falta de garantías de tiempo, etc.).

No creo que RAII sea la mejor forma de tratar con la memoria en todos los casos. En varias ocasiones, codificar su programa en una implementación GC genuina y eficiente (piense en Ocaml o SBCL) puede ser más simple (para desarrollar) y más rápido (para ejecutar) que codificarlo con un elegante estilo RAII en C ++ 17. En otros casos, no lo es. YMMV.

Por ejemplo, si codifica un intérprete de Scheme en C ++ 17 con el estilo RAII más sofisticado, igual tendría que codificar (o usar) un explícito GC dentro de él (porque un montón de esquema tiene circulares). Y la mayoría asistentes de prueba están codificados en lenguajes GC-ed, a menudo funcionales, (el único que conozco codificado en C ++ es Apoyarse) por buenas razones.

Por cierto, estoy interesado en encontrar una implementación de Scheme C ++ 17 (pero menos interesado en codificarlo yo mismo), preferiblemente con alguna habilidad de multi-threading.


14
2018-06-02 09:20



RAII y GC resuelven problemas en direcciones completamente diferentes. Son completamente diferentes, a pesar de lo que dirían algunos.

Ambos abordan el problema de que administrar recursos es difícil. Garbage Collection lo resuelve haciendo que el desarrollador no tenga que prestar tanta atención a la administración de esos recursos. RAII lo resuelve haciendo que sea más fácil para los desarrolladores prestar atención a su gestión de recursos. Cualquiera que diga que hace lo mismo tiene algo que venderte.

Si observa las tendencias recientes en idiomas, verá que ambos enfoques se utilizan en el mismo idioma porque, francamente, realmente necesita ambos lados del rompecabezas. Está viendo muchos idiomas que usan algún tipo de recolección de basura para no tener que prestar atención a la mayoría de los objetos, y esos lenguajes también ofrecen soluciones RAII (como python's with operador) para las veces que realmente desea prestarles atención.

  • C ++ ofrece RAII a través de constructores / destructores y GC a través de shared_ptr (Si puedo argumentar que refcounting y GC están en la misma clase de soluciones porque ambas están diseñadas para ayudarlo a no tener que prestar atención a la esperanza de vida)
  • Python ofrece RAII a través de with y GC a través de un sistema de recuento más un recolector de basura
  • C # ofrece RAII a través de IDisposable y using y GC a través de un recolector de basura generacional

Los patrones están apareciendo en todos los idiomas.


13
2018-06-02 23:45



Uno de los problemas con los recolectores de basura es que es difícil predecir el rendimiento del programa.

Con RAII usted sabe que en el momento exacto en que el recurso saldrá del alcance, borrará algo de memoria y le llevará algo de tiempo. Pero si no eres un maestro en la configuración del recolector de basura, no puedes predecir cuándo ocurrirá la limpieza.

Por ejemplo: la limpieza de un montón de objetos pequeños se puede hacer de manera más efectiva con GC, ya que puede liberar gran parte, pero no será de operación rápida, y es difícil predecir cuándo ocurrirá y debido a la "gran limpieza del trozo" lo hará tome algo de tiempo de procesador y puede afectar el rendimiento de su programa.


10
2018-06-02 09:19



Mas o menos. El modismo RAII puede ser mejor para el estado latente y estar nervioso. Un recolector de basura puede ser mejor para el sistema rendimiento.


9
2018-06-02 09:19



"Eficiente" es un término muy amplio, en el sentido de los esfuerzos de desarrollo RAII es típicamente menos eficiente que GC, pero en términos de rendimiento, GC es típicamente menos eficiente que RAII. Sin embargo, es posible proporcionar contr-examples para ambos casos. Tratar con GC genérico cuando tiene patrones de asignación de recursos (de) muy claros en los lenguajes administrados puede ser bastante problemático, al igual que el código que usa RAII puede ser sorprendentemente ineficiente cuando shared_ptr se usa para todo sin ninguna razón.


5
2018-06-02 09:26



La parte principal de la pregunta sobre si uno u otro es "beneficioso" o más "eficiente" no puede ser respondida sin dar mucho contexto y argumentar sobre las definiciones de estos términos.

Más allá de eso, básicamente puedes sentir la tensión del antiguo "¿Es Java o C ++ el mejor lenguaje?" flamewar crepitante en los comentarios. Me pregunto cómo sería una respuesta "aceptable" a esta pregunta, y tengo curiosidad por verla eventualmente. 

Pero un punto sobre una posible importancia conceptual la diferencia aún no se ha señalado: con RAII, está vinculado al hilo que llama al destructor. Si su aplicación es de un solo hilo (y aunque fue Herb Sutter quien afirmó que El almuerzo gratis ha terminado: La mayoría de los softwares hoy en día todavía es single-threaded), entonces un solo núcleo puede estar ocupado manejando las limpiezas de objetos que ya no son relevantes para el programa real ...

En contraste con eso, el recolector de basura por lo general se ejecuta en su propio hilo, o incluso múltiples hilos, y por lo tanto está (en cierta medida) desacoplado de la ejecución de las otras partes.

(Nota: Algunas respuestas ya intentaron señalar los patrones de aplicación con diferentes características, eficiencia, rendimiento, latencia y rendimiento mencionados, pero este punto específico no se mencionó todavía)


5
2018-06-03 01:32



RAII trata de manera uniforme todo lo que se puede describir como un recurso. Las asignaciones dinámicas son uno de esos recursos, pero de ninguna manera son el único, y podría decirse que no el más importante. Los archivos, los sockets, las conexiones de bases de datos, la retroalimentación de la interfaz gráfica de usuario y más son todos los elementos que se pueden gestionar de manera determinista con RAII.

Los GC solo se ocupan de las asignaciones dinámicas, lo que alivia al programador de preocuparse por el volumen total de objetos asignados a lo largo de la vida útil del programa (solo deben preocuparse por el ajuste máximo del volumen de asignación simultánea).


4
2018-06-02 13:03