Realmente me gusta la administración de memoria basada en el alcance (SBMM), o RAII , ya que la comunidad C ++ se refiere más comúnmente (¿confusamente?). Hasta donde yo sé, a excepción de C ++ (y C), no hay otro lenguaje convencional en uso hoy en día que haga de SBMM / RAII su mecanismo principal de administración de memoria, y en su lugar prefieren usar la recolección de basura (GC).
Esto me parece bastante confuso, ya que
- SBMM hace que los programas sean más deterministas (puede saber exactamente cuándo se destruye un objeto);
- en los lenguajes que usan GC, a menudo tiene que hacer una gestión manual de recursos (vea cerrar archivos en Java, por ejemplo), lo que en parte anula el propósito de GC y también es propenso a errores;
- La memoria de almacenamiento dinámico también puede (muy elegantemente, imo) estar unida al alcance (ver
std::shared_ptr
en C ++).
¿Por qué no se usa más SBMM? ¿Cuáles son sus desventajas?
finalize()
se llamará al método de un objeto antes de la recolección de basura. En efecto, esto crea la misma clase de problema que se supone que debe resolver la recolección de basura.Respuestas:
Comencemos por postular que la memoria es, con diferencia (docenas, cientos o incluso miles de veces) más común que todos los demás recursos combinados. Cada variable, objeto, miembro de objeto necesita un poco de memoria asignada y liberada más adelante. Por cada archivo que abra, crea decenas de millones de objetos para almacenar los datos extraídos del archivo. Cada secuencia TCP va junto con un número ilimitado de cadenas de bytes temporales creadas para escribirse en la secuencia. ¿Estamos en la misma página aquí? Excelente.
Para que RAII funcione (incluso si tiene punteros inteligentes listos para cada caso de uso bajo el sol), debe obtener la propiedad correcta. Debe analizar quién debe poseer este o aquel objeto, quién no debe hacerlo y cuándo debe transferirse la propiedad de A a B. Claro, puede usar la propiedad compartida para todo , pero entonces estaría emulando un GC a través de punteros inteligentes. En ese momento, es mucho más fácil y rápido incorporar el GC al lenguaje.
La recolección de basura lo libera de esta preocupación por el recurso más utilizado, la memoria. Claro, aún necesita tomar la misma decisión para otros recursos, pero esos son mucho menos comunes (ver arriba), y la propiedad complicada (por ejemplo, compartida) también es menos común. La carga mental se reduce significativamente.
Ahora, nombra algunos inconvenientes para hacer que se recojan todos los valores. Sin embargo, la integración de GC y tipos de valores seguros para la memoria con RAII en un idioma es extremadamente difícil, por lo que ¿quizás sea mejor migrar estas compensaciones por otros medios?
La pérdida de determinismo resulta no ser tan mala en la práctica, porque solo afecta la vida del objeto determinista . Como se describe en el siguiente párrafo, la mayoría de los recursos (aparte de la memoria, que es abundante y puede reciclarse de manera bastante perezosa) no están sujetos a la vida útil de los objetos en estos idiomas. Hay algunos otros casos de uso, pero son raros en mi experiencia.
Su segundo punto, la gestión manual de recursos, se aborda hoy en día a través de una declaración que realiza una limpieza basada en el alcance, pero no combina esta limpieza con el tiempo de vida del objeto (por lo tanto, no interactúa con el GC y la seguridad de la memoria). Esto está
using
en C #,with
en Python,try
-with-resources en versiones recientes de Java.fuente
using
las declaraciones solo son posibles localmente. Es imposible limpiar los recursos contenidos en las variables miembro de esa manera.using
es una broma en comparación con RAII, para que lo sepas.RAII también se deduce de la gestión automática de memoria de conteo de referencias, por ejemplo, como la utilizada por Perl. Si bien el recuento de referencias es fácil de implementar, determinista y bastante eficaz, no puede tratar con referencias circulares (causan una fuga), por lo que no se usa comúnmente.
Los lenguajes recolectados de basura no pueden usar RAII directamente, pero a menudo ofrecen sintaxis con un efecto equivalente. En Java, tenemos la declaración try-with-ressource
que llama automáticamente
.close()
al recurso a la salida del bloque. C # tiene laIDisposable
interfaz, que permite.Dispose()
que se llame al salir de unausing (...) { ... }
declaración. Python tiene lawith
declaración:que funciona de manera similar. En un giro interesante sobre esto, el método de apertura de archivos de Ruby recibe una devolución de llamada. Una vez que se ha ejecutado la devolución de llamada, el archivo se cierra.
Creo que Node.js usa la misma estrategia.
fuente
with-open-filehandle
funciones que abren el archivo, lo ceden a una función y al regresar de la función cierran el archivo nuevamente.En mi opinión, la ventaja más convincente de la recolección de basura es que permite la componibilidad . La corrección de la administración de memoria es una propiedad local en el entorno de recolección de basura. Puede ver cada parte de forma aislada y determinar si puede perder memoria. Combine cualquier número de partes con corrección de memoria y se mantendrán correctas.
Cuando confía en el recuento de referencias, pierde esa propiedad. Si su aplicación puede perder memoria se convierte en una propiedad global de toda la aplicación con recuento de referencias. Cada nueva interacción entre partes tiene la posibilidad de usar la propiedad incorrecta y romper la administración de memoria.
Tiene un efecto muy visible en el diseño de programas en los diferentes idiomas. Los programas en los lenguajes GC tienden a ser un poco más sopas de objetos con muchas interacciones, mientras que en los lenguajes sin GC uno tiende a preferir partes estructuradas con interacciones estrictamente controladas y limitadas entre ellos.
fuente
Los cierres son una característica esencial de casi todos los idiomas modernos. Son muy fáciles de implementar con GC y muy difíciles (aunque no imposibles) de acertar con RAII, ya que una de sus características principales es que le permiten abstraer durante la vida útil de sus variables.
C ++ solo los obtuvo 40 años después de que todos los demás lo hicieran, y a mucha gente inteligente le costó mucho trabajo hacerlos bien. En contraste, muchos lenguajes de scripting diseñados e implementados por personas con cero conocimiento en el diseño e implementación de lenguajes de programación los tienen.
fuente
[&]
sintaxis. Cualquier programador de C ++ ya asocia el&
signo con referencias y conoce referencias obsoletas.Para la mayoría de los programadores, el sistema operativo no es determinista, su asignador de memoria no es determinista y la mayoría de los programas que escriben son concurrentes y, por lo tanto, inherentemente no deterministas. Agregar la restricción de que un destructor se llama exactamente al final del alcance en lugar de un poco antes o un poco después no es un beneficio práctico significativo para la gran mayoría de los programadores.
Ver
using
en C # yuse
en F #.En otras palabras, podría tomar el montón que es una solución de propósito general y cambiarlo para que solo funcione en un caso específico que sea seriamente limitante. Eso es cierto, por supuesto, pero inútil.
SBMM limita lo que puede hacer:
SBMM crea el problema del funarg ascendente con los cierres léxicos de primera clase, por lo que los cierres son populares y fáciles de usar en lenguajes como C #, pero raros y difíciles en C ++. Tenga en cuenta que existe una tendencia general hacia el uso de construcciones funcionales en la programación.
SBMM requiere destructores e impiden las llamadas de cola agregando más trabajo por hacer antes de que una función pueda regresar. Las llamadas de cola son útiles para máquinas de estado extensibles y son proporcionadas por cosas como .NET.
Algunas estructuras de datos y algoritmos son notoriamente difíciles de implementar usando SBMM. Básicamente, en cualquier lugar donde los ciclos ocurren naturalmente. En particular, los algoritmos gráficos. Efectivamente terminas escribiendo tu propio GC.
La programación concurrente es más difícil porque el flujo de control y, por lo tanto, la vida útil de los objetos son inherentemente no deterministas aquí. Las soluciones prácticas en los sistemas de transmisión de mensajes tienden a ser una copia profunda de los mensajes y el uso de vidas excesivamente largas.
SBMM mantiene los objetos vivos hasta el final de su alcance en el código fuente, que a menudo es más largo de lo necesario y puede ser mucho más de lo necesario. Esto aumenta la cantidad de basura flotante (objetos inalcanzables que esperan ser reciclados). Por el contrario, el rastreo de recolección de basura tiende a liberar objetos poco después de que la última referencia a ellos desaparezca, lo que puede ser mucho antes. Consulte Mitos sobre la gestión de la memoria: rapidez .
SBMM es tan limitante que los programadores necesitan una ruta de escape para situaciones en las que no se pueden anidar vidas. En C ++,
shared_ptr
ofrece una ruta de escape, pero puede ser ~ 10 veces más lenta que el rastreo de recolección de basura . Por lo tanto, usar SBMM en lugar de GC pondría a la mayoría de las personas equivocadas la mayor parte del tiempo. Sin embargo, eso no quiere decir que sea inútil. SBMM sigue siendo valioso en el contexto de sistemas y programación integrada donde los recursos son limitados.FWIW te gustaría echar un vistazo a Forth y Ada, y leer sobre el trabajo de Nicolas Wirth.
fuente
shared_ptr
solo es raro en C ++ porque es muy lento. En segundo lugar, se trata de una comparación de manzanas y naranjas (como ya mostró el artículo que cité) porqueshared_ptr
es muchas veces más lenta que un GC de producción. En tercer lugar, los GC no son omnipresentes y se evitan en software como LMax y el motor FIX de Rapid Addition.Mirando un índice de popularidad como TIOBE (que es discutible, por supuesto, pero supongo que para su tipo de pregunta está bien usar esto), primero ve que ~ 50% de los 20 principales son "lenguajes de script" o "dialectos SQL ", donde la" facilidad de uso "y los medios de abstracción tienen mucha más importancia que el comportamiento determinista. Del resto de los idiomas "compilados", hay alrededor del 50% de los idiomas con SBMM y ~ 50% sin ellos. Entonces, cuando saque los lenguajes de scripting de su cálculo, diría que su suposición es incorrecta, entre los lenguajes compilados los que tienen SBMM son tan populares como los que no.
fuente
Una ventaja importante de un sistema GC que nadie ha mencionado todavía es que se garantiza que una referencia en un sistema GC retendrá su identidad mientras exista . Si se llama
IDisposable.Dispose
(.NET) oAutoCloseable.Close
(Java) en un objeto mientras existen copias de la referencia, esas copias continuarán haciendo referencia al mismo objeto. El objeto ya no será útil para nada, pero los intentos de usarlo tendrán un comportamiento predecible controlado por el propio objeto. Por el contrario, en C ++, si el código invocadelete
un objeto y luego intenta usarlo, todo el estado del sistema queda totalmente indefinido.Otra cosa importante a tener en cuenta es que la administración de memoria basada en el alcance funciona muy bien para objetos con una propiedad claramente definida. Funciona mucho menos bien, y a veces francamente mal, con objetos que no tienen una propiedad definida. En general, los objetos mutables deben tener propietarios, mientras que los objetos inmutables no necesitan, pero hay una arruga: es muy común que el código use una instancia de tipos mutables para contener datos inmutables, asegurando que no se exponga ninguna referencia a código que podría mutar la instancia. En tal escenario, las instancias de la clase mutable pueden compartirse entre múltiples objetos inmutables y, por lo tanto, no tienen una propiedad clara.
fuente
En primer lugar, es muy importante darse cuenta de que equiparar RAII a SBMM. o incluso a SBRM. Una de las cualidades más esenciales (y menos conocidas o menos apreciadas) de RAII es el hecho de que 'ser un recurso' es una propiedad que NO es transitiva a la composición.
La siguiente publicación de blog discute este aspecto importante de RAII y lo contrasta con la administración de recursos en lenguajes GC que usan GC no determinista.
http://minorfs.wordpress.com/2011/04/29/why-garbage-collection-is-anti-productive/
Es importante tener en cuenta que aunque RAII se usa principalmente en C ++, Python (por fin la versión no basada en VM) tiene destructores y GC determinista que permite que RAII se use junto con GC. Lo mejor de ambos mundos si lo fuera.
fuente
File.ReadLines file |> Seq.length
donde las abstracciones manejan el cierre para mí. Cerraduras e hilos que he reemplazado con .NETTask
y F #MailboxProcessor
. Todo este "explotamos la cantidad de gestión manual de recursos" es un completo disparate.