4 puntos por GN⁺ 2024-03-31 | 1 comentarios | Compartir por WhatsApp
  • Incluso en código donde el rendimiento y la concurrencia son cruciales, como el kernel y los drivers, liberar memoria no termina con una simple llamada a free(), y puede ser necesario usar técnicas de compartición sin locks
  • RCU(Read, Copy, Update) es un enfoque en el que se copian datos que se leen con frecuencia y cambian rara vez, y luego se reemplaza el puntero de forma atómica, sin bloquear la ruta de lectura
  • Si se hace delete de la versión anterior de inmediato, puede ocurrir un use-after-free en un hilo que todavía la esté leyendo, por lo que RCU rastrea las secciones de lectura y retrasa la liberación hasta un momento seguro
  • RCU se usa decenas de miles de veces en Linux, también existe en la librería Folly de C++ y en crossbeam-epoch de Rust, y al limpiar más tarde según si sigue en uso, adopta una forma similar al GC
  • La dicotomía de que la gestión manual de memoria siempre es más rápida y predecible es débil; free(), el conteo de referencias y el comportamiento de memoria del SO también tienen costos e incertidumbre, así que el GC moderno también puede ser una herramienta para la programación de sistemas

Por qué el código del kernel usa técnicas como RCU

  • El sistema operativo está entre los programas que se ejecutan todos los días con mayor sensibilidad al rendimiento
  • Si el SO se vuelve más rápido, el usuario puede hacer más cómputo, así que quienes desarrollan el kernel y los drivers invierten mucho esfuerzo en optimizar el código
  • Un sistema operativo no solo maneja procesos e hilos del espacio de usuario, sino también varios hilos del propio kernel y hasta handlers de interrupciones de hardware
  • Como aumentar la latencia le roba tiempo al usuario, en el código del kernel surgieron varias técnicas para compartir datos entre hilos sin locks

Funcionamiento básico de RCU

  • RCU(Read, Copy, Update) es un enfoque adecuado para datos que se leen muchísimo pero se escriben rara vez
    • Un ejemplo sería el conjunto de dispositivos USB conectados actualmente: casi no cambia, pero puede cambiar
    • El cambio debe ocurrir de forma atómica y no debe bloquear a lectores que ya estén leyendo
  • Quien escribe actualiza el estado compartido en este orden
    • Lee los datos actuales desde el puntero
    • Copia los datos actuales y aplica los cambios necesarios para crear una nueva versión
    • Actualiza el puntero de forma atómica para que apunte a la nueva versión
  • Quien lee solo necesita leer el puntero compartido, así que la ruta de lectura es simple y funciona sin esperas
  • Este enfoque es fácil de usar y wait-free, pero si no se limpia la versión anterior se produce una fuga de memoria

La versión anterior no puede liberarse de inmediato

  • Si justo después de cambiar el puntero a la nueva versión se hace delete de la anterior, aparece el riesgo de use-after-free
  • Como funciona sin locks, quien escribe no puede saber si todavía hay lectores usando la versión anterior
  • Quien lee puede marcar una sección crítica del lado lector con rcu_read_lock() y rcu_read_unlock()
    • El lector sigue sin bloquearse
    • Quien escribe no elimina los datos anteriores hasta que esos lectores hayan salido
  • rcu_synchronize() no necesita esperar a que desaparezcan todos los lectores, sino solo a que terminen los lectores anteriores que todavía podrían ver la versión previa
    • Los lectores que ya vieron el nuevo puntero usan la nueva versión, así que no afectan la vida útil de la versión anterior

Liberación diferida y una forma de GC

  • Aunque quien escribe no espere dentro de la función de actualización, el código sigue siendo correcto si los datos anteriores se liberan de forma segura en algún momento
  • Un esquema como rcu_defer(old) permite liberar old en cualquier momento después de que los lectores actuales hayan salido de su sección crítica
  • Una forma en la que un hilo dedicado limpia periódicamente versiones antiguas ya no referenciadas se parece a un GC generacional
  • RCU no es un experimento mental, sino una técnica muy usada en la práctica
    • Linux usa RCU decenas de miles de veces
    • Facebook ofrece RCU en la librería Folly de C++
    • En Rust se usa con el nombre de crossbeam-epoch y sirve de base para librerías de concurrencia populares
  • Más importante que discutir si RCU es “GC de verdad” es que su estructura, donde la memoria se limpia después según si sigue en uso, es la misma idea que en GC

Los costos ocultos de la liberación manual

  • La idea de que el GC es inherentemente menos eficiente que la gestión manual de memoria se debilita cuando se miran los detalles de implementación
  • free() no es gratis

    • Un asignador de memoria de propósito general tiene que gestionar estado global interno, como páginas obtenidas del kernel, partición por buckets de tamaño y buckets en uso
    • Puede haber contención cuando varios hilos intentan bloquear el estado del asignador
    • Incluso si se usan pools locales por hilo como en jemalloc, hace falta código adicional para sincronizarlos
  • RAII y los lifetimes tampoco eliminan el costo del asignador

    • Los lifetimes de Rust o el RAII de C++ ayudan a automatizar y hacer correcta la liberación de memoria, pero no eliminan la complejidad de las estructuras internas del asignador
    • En muchos escenarios hay que volver a shared_ptr o Arc
    • Se necesita metadato adicional en forma de conteo de referencias, y ese valor puede moverse entre núcleos y cachés generando costo
    • También puede hacer que ciclos en el grafo de liveness terminen en fugas
  • El GC también puede ofrecer optimizaciones

    • Un GC generacional con movimiento recompone periódicamente el heap
    • La asignación puede acercarse a simplemente incrementar un puntero, logrando alto throughput
    • La localidad de la asignación secuencial también puede ayudar al rendimiento de caché

Equívocos sobre el control de la gestión de memoria

  • Muchos desarrolladores que se oponen al GC están construyendo sistemas de tiempo real suave
    • Quieren que funcione lo más rápido posible, como los FPS de un videojuego o el rendimiento de compresión de un códec de streaming
    • Pero no tienen requisitos de latencia dura donde una demora ocasional de 1 milisegundo rompa el sistema o cueste una vida
  • La creencia de que el programador puede decidir exactamente cuándo ocurre la gestión de memoria no es tan simple
    • El sistema operativo abstrae la interacción con el hardware
    • Linux, por defecto, casi no hace nada cuando se pide memoria y puede entregarla recién cuando realmente se intenta usar
    • Si se mezclan madvise(), I/O mapeado en memoria y caché del sistema de archivos, no hay una respuesta simple a “qué se asignó y cuándo”
    • En un mal día, un simple acceso a puntero puede terminar en I/O de disco
  • También es limitada la idea de que el programador siempre sabe cuál es un buen momento para pausar y gestionar memoria
    • Hay casos claros, como la pantalla de carga de un videojuego
    • En mucho software, la única respuesta es cuando no se está ocupado con algo más importante
    • El código individual que usa shared_ptr y Arc no puede saber de antemano si le tocará ser el último dueño y encargarse de la limpieza
  • Tampoco siempre es cierto que llamar a free() devuelva de inmediato la memoria al SO
    • La memoria se asigna desde el SO en unidades de página
    • A menudo el asignador retiene páginas para reutilizarlas antes de que termine el programa
    • El SO también puede recuperar páginas mediante swap

Por qué el GC puede verse como una herramienta de programación de sistemas

  • No todo el software se beneficia del GC
  • Pero incluso cerca de 2024, entre programadores de sistemas la discusión sobre GC suele quedar enterrada bajo falsas dicotomías y miedo, incertidumbre y duda
  • La idea de que los lenguajes con GC son “claramente” más lentos que los lenguajes con gestión manual de memoria no es un hecho, sino algo más cercano a una ideología
  • Incluso en equipos que construyen sistemas donde hay vidas en juego, ha habido casos de latencia submicrosegundo usando lenguajes con GC que asignan en casi todas las líneas
  • Si una parte del sistema necesariamente debe ejecutarse dentro de n ciclos de reloj, esa parte específica puede separarse en código sin GC o en hardware
  • El GC no es una solución mágica, pero sí una herramienta dentro de la caja de herramientas que puede usarse sin miedo

1 comentarios

 
GN⁺ 2024-03-31
Comentarios en Hacker News
  • Entre las técnicas modernas prometedoras de recolección de basura paralela vale la pena ver MPL o MaPLe y su nuevo trabajo sobre Automatic Management of Parallelism
    La idea central de los trabajos que recibieron el POPL 2024 distinguished paper award y el ACM SIGPLAN dissertation award 2023 es doble: a) recolección de basura paralela demostrablemente eficiente basada en disentanglement, b) control automático de granularidad demostrablemente eficiente
    [1] MaPLe (MPL): https://github.com/MPLLang/mpl
    [2] Automatic Parallelism Management: https://dl.acm.org/doi/10.1145/3632880

    • Standard ML y su comunidad han hecho contribuciones bastante impresionantes a la literatura sobre administración de memoria
      Además del artículo enlazado, también está MLKit, que fue uno de los primeros usuarios y pioneros de la administración de memoria basada en regiones
    • Me pregunto qué significa exactamente eso de “demostrablemente eficiente”
    • Me gustaría saber cómo se compara este enfoque con el trabajo reciente de soporte de paralelismo multicore en OCaml
    • Quisiera preguntarles a quienes saben más cuánto de esto podría aplicarse a otros lenguajes
      Por ejemplo, ¿un enfoque así podría hacer mucho más rápido el recolector de basura de Go, o chocaría con limitaciones del diseño existente del lenguaje?
  • Los casos de uso de RCU son convincentes, pero la experiencia con recolección de basura en otras situaciones no ha sido buena.
    Este texto se lee menos como una afirmación de que la vida útil estática es generalmente mejor que la dinámica, y más como una afirmación de que las soluciones personalizadas de gestión de memoria pueden dar el mejor rendimiento.
    No creo que free() devuelva memoria al sistema operativo, sino al asignador. Eso es mucho mejor que devolverla al sistema operativo, y las llamadas al sistema son lentas. Aun así, un asignador como mimalloc también puede hacer que la memoria liberada no esté disponible de inmediato para el siguiente malloc, sino solo periódicamente.
    Al asignar 800 bytes y luego hacer free inmediatamente, repitiendo eso un millón de veces y contando el número de punteros únicos, glibc malloc dio 1, jemalloc dio 1, mimalloc dio 4, y el recolector de basura de Julia dio 62767.
    62767, unos 48 MiB, no está tan mal, pero aun así expulsa la caché L3 de mi computadora. Si usas recolección de basura, casi está garantizado que las nuevas asignaciones vendrán de RAM y no de caché, lo que arruina el rendimiento del código con muchas asignaciones. No solo importa la velocidad de la gestión de memoria en sí, sino también qué tan rápido puedes trabajar con la memoria que te dio.
    Publiqué un benchmark en Julia para mostrar esto: https://discourse.julialang.org/t/blog-post-rust-vs-julia-in...
    malloc/free le da a la memoria de trabajo real la oportunidad de mantenerse caliente en caché si es lo bastante pequeña. Los asignadores como mimalloc también están diseñados para colocar cerca las asignaciones contiguas, como hace un recolector de basura con compactación, y los 4 punteros únicos que vi en mimalloc estaban separados por 896 bytes.
    Si hubiera tenido más experiencia con recolección de basura con compactación, quizá pensaría de forma menos cínica, pero considero que la recolección de basura es una solución mucho más compleja que algo como el verificador de préstamos de Rust para lograr una gestión de memoria segura. Esa complejidad se les traslada a los desarrolladores del compilador y del runtime, así que normalmente los usuarios están bien con eso, y es una compensación aceptable cuando escribes código que no es sensible al rendimiento. RAII con vida útil estática también es una compensación razonable para código que no necesita un enfoque más personalizado, y el ejemplo del artículo claramente es un caso que sí necesita una solución personalizada.

    • No basta con decir que, como la recolección de basura dio más punteros, la localidad de caché es peor.
      La recolección de basura con compactación casi siempre tiene mejor uso de caché que malloc en programas de larga duración, porque la fragmentación del heap desperdicia entradas de caché TLB y espacio libre entre objetos. El asignador tipo bump de un recolector de basura con compactación da un puntero nuevo en cada asignación porque free no recupera memoria, pero esas asignaciones siguen siendo secuenciales y, si el heap sigue consumiéndose mientras solo se tocan los objetos más recientes, igual se mantienen en caché. Hacer benchmarks del efecto cascada del asignador y del recolector de basura es extremadamente difícil, y casi siempre veo este tipo de benchmarks sintéticos con escepticismo.
    • El artículo explica por qué este enfoque funciona bien en el contexto de RCU, por qué en general no tanto, y luego simplemente lo deja de lado y lo ignora.
      Lo que preocupa a la gente es que el proceso de averiguar si la memoria ya no se usa sea menos eficiente y no determinista en comparación con decirle directamente al asignador que ya terminaste de usar el recurso. Nunca he visto a nadie preocupado por el simple hecho de retrasar la liberación.
      Recorrer todo el conjunto vivo sucede rara vez, y tras 30 años mejorando algoritmos de recolección de basura hemos llegado casi a un nivel de magia negra perceptible, pero esta frase ignora, intencionalmente o no, el punto que a la gente realmente le preocupa. Cuando hay problemas de recolección de basura en un servicio, se siente como si hubiera que invocar a un chamán para ajustar cosas por todos lados y esperar que las almas furiosas regresen al mundo de las sombras.
      Si se marcara la basura y se recibiera una notificación cuando dejara de usarse, todo ese proceso desaparecería. La asignación de memoria puede ser muy rápida con recolección de basura, pero para una comparación justa también hay que incluir, amortizados, los costos de marcado y compactación.
      Otro gran problema es que, para obtener el mismo rendimiento, la recolección de basura por lo general requiere mucha más memoria que la gestión manual. También hace falta CPU extra para verificar repetidamente y de forma redundante si todavía existen referencias, y además hay que aceptar copias adicionales de memoria para una compactación optimista.
      Por último, el artículo critica que cuando la vida útil no está clara se necesita gestión manual de memoria como Arc/Rc de Rust, pero ignora que en los lenguajes con recolección de basura tampoco se puede confiar en que un finalizer vaya a ejecutarse sí o sí, así que en la práctica construyen casi la misma infraestructura para cerrar recursos externos.
      Este debate ya se ha repetido bastante en los últimos 20 o 30 años, y este artículo no parece aportar nada nuevo más allá de descartar preocupaciones legítimas sobre la recolección de basura tratándolas como memes. Los memes son divertidos, así que está bien, pero la respuesta correcta es que no existe una respuesta general. Solo hay que usar la herramienta adecuada para satisfacer las restricciones de diseño del sistema.
    • No entiendo bien este argumento. En la recolección de basura generacional, es muy probable que la generación 0 esté en caché, y precisamente ahí ocurre la mayoría de la creación y descarte.
      Las demás asignaciones más longevas, por definición, no son fáciles de controlar desde el punto de vista de caché. La localidad es una de las grandes ventajas de la recolección de basura, y el único problema que conozco es el marcado/barrido de detención total. Sé que los recolectores modernos tienen hilos en segundo plano, pero según entiendo, igual siguen ocurriendo eventos de detención total.
    • Es cierto que free() devuelve memoria al asignador, pero lidiar con la fragmentación de memoria en servidores de larga ejecución no es nada agradable.
      En particular la fragmentación interna de las páginas gestionadas por asignadores slab; no es un problema común, pero sí uno difícil de manejar.
    • Si el uso de caché es una preocupación tan grande, la asignación por arena funciona bien igual que en la asignación manual de memoria.
      Por suerte, no hay muchas áreas donde la recolección de basura tenga que competir con ejemplos tan convenientemente armados como este.
  • Salvo en el caso especial donde toda la memoria pueda manejarse fácilmente con arenas, un buen garbage collector de trazado superó hace tiempo a la gestión manual de memoria en rendimiento, y recientemente su impacto en la latencia también es lo bastante aceptable para la gran mayoría de las aplicaciones
    El ZGC de OpenJDK suele tener pausas del orden de decenas o cientos de microsegundos, y con tasas de asignación razonables casi nunca supera 1 ms ni siquiera en el peor caso, quedando en un rango comparable a las pausas provocadas por el sistema operativo
    La única compensación realmente importante es el uso de memoria. Salvo en nichos especiales, es decir, cuando las arenas encajan bien para todo y la peor latencia está en el rango bajo de microsegundos, la pregunta clave es una sola: ¿mi aplicación corre en un entorno con restricciones de memoria, o vale la pena sacrificar otras cosas para reducir el uso de RAM?

    • En mi experiencia es al revés. Las vidas útiles individuales por objeto son un caso especial poco común, y en la mayor parte del código real hay muchos objetos relacionados con vidas útiles iguales o muy parecidas
      En ese tipo de código, rastrear la vida útil de cada objeto individual es excesivo. Al final, la gestión de memoria trata sobre tiempos de vida, y menos vidas útiles individuales siempre es mejor que más, porque reduce el trabajo a hacer, ya sea manual o automático
      No tener que pensar en la vida útil de los objetos es muy conveniente, y por eso los lenguajes con garbage collection han tenido éxito, aunque la complejidad interna de un buen recolector sea considerable
    • Las pausas están resueltas hasta cierto punto, pero el uso de CPU del garbage collection sigue siendo bastante alto
      También sigue sufriendo por la latencia de cola impredecible y por el impacto de varios casos excepcionales
    • En el trabajo estamos haciendo benchmarks de ZGC y Shenandoah, y el tiempo de pausa p100 suele estar por debajo de 500 us
      ZGC parece tener menos pausas que Shenandoah, así que da la impresión de que hace más trabajo por cada pausa y por eso muestra un rendimiento algo mejor
      Aún falta probarlo en producción, pero hasta ahora parece que con ZGC, y con el ZGC generacional a partir de Java 21, las pausas por garbage collection son en gran medida un problema resuelto
    • Parece injusto exigir que toda la memoria pueda manejarse fácilmente con arenas. Si la mayoría de los objetos puede asignarse fácilmente con arenas, eso por sí solo ya elimina gran parte de la necesidad de garbage collection
      Desde una perspectiva como la de Jai, la asignación de memoria puede dividirse en cuatro tipos, en orden de frecuencia: 1) cosas de vida extremadamente corta que pueden ir en el stack de la función, 2) cosas de vida corta con una vida útil bien definida que pueden ir en una arena de memoria por frame o por request, 3) cosas de vida larga con un propietario bien definido que pueden gestionarse en pools dedicados por subsistema, 4) cosas de vida larga con propietario poco claro que requieren gestión dinámica de memoria
      Para afirmar que el garbage collection de trazado normalmente supera a la gestión manual de memoria, no hay que compararlo con un sistema que llama a malloc/free por todas partes, sino con uno escrito teniendo en mente este tipo de enfoque. Compararlo con prácticas modernas de C++/Rust sería más justo
      Sí coincido en que, en la mayoría de los sistemas, depender del garbage collection de trazado probablemente sea mucho más práctico, pero esa es una afirmación completamente distinta
    • Hace falta evidencia
  • El texto motiva RCU y luego da un giro para empezar a defender el garbage collection de propósito general de forma amplia
    No llega a ser un caballo de Troya, pero sí se siente como un cambio bastante brusco

    • Yo no llamaría garbage collection a RCU. En ningún momento los objetos están en estado de basura
      Los objetos están en uno de tres estados y cambian tan rápido como sea posible: active, obsolete but alive for old readers, deallocated
      Dependiendo de cómo se escriba el código, quizá también sea seguro reutilizar objetos “obsolete-but-alive” para nuevas asignaciones con new, aunque no he analizado a fondo el rendimiento
      Como suele pasar en las discusiones sobre garbage collection, queda muy poco claro en qué momento hay que “retroceder” a shared_ptr/Arc. En la práctica, la clave de un sistema serio basado en conteo de referencias es evitar el conteo de referencias: demostrar que ya existe ownership o evitar por completo la indirección. No hacer nada es, por supuesto, mejor que el “hacer algo en algún momento” del garbage collection
  • En el software que uso hay dos casos. (1) rutas calientes donde siempre se usan allocators personalizados y se evita asignar, (2) todo lo demás
    En el caso (1), con garbage collection o sin él da igual, porque yo me voy a salir. En el caso (2), el garbage collection es realmente conveniente y correcto

    • De acuerdo. En un lugar donde usábamos Java/C++ intentamos resolver esta división con interoperabilidad, pero al final nos trajo más problemas de los que resolvió
      Lo que Java ha logrado con recolectores modernos es impresionante, pero incluso ellos reconocen indirectamente, a través de Valhalla, que el código sin asignaciones o con pocas asignaciones tiene su lugar
  • La observación de que los sistemas operativos modernos para usuarios, es decir, sistemas operativos que no son RTOS especiales, tienen recolección de basura integrada se desvía un poco aquí
    Simplemente no lo llamamos así, sino administración de memoria. ¿Cómo se llama a un lenguaje que tiene recolección de basura integrada? Un lenguaje con administración de memoria
    Esto se ve a menudo en programas antiguos en C que se ejecutan “de arriba hacia abajo”. Asignan, limpian los recursos del sistema, pero no se preocupan por free. Cuando el programa termina, el sistema operativo recupera toda esa memoria, así que ¿para qué molestarse?
    Aquí hay una oportunidad para crear un sistema operativo que maneje recursos como un recolector de basura del runtime de un lenguaje, con un recolector de basura a nivel de sistema operativo menos aislado del programa. Pero normalmente, en los lenguajes con recolección de basura, el recolector está tan complejamente entrelazado con casi cada línea del runtime que no es práctico hacer que solo una distribución para un sistema operativo entregue ese control al sistema operativo
    Aun así, es una lástima. Porque hay mucho margen para mejorar problemas crónicos causados por la separación artificial entre la administración de memoria a nivel de programa y la administración de memoria a nivel de sistema operativo

    • Como el sistema operativo se encarga de ello cuando el programa termina, no hace falta liberar no solo memoria sino también otros recursos como archivos, sockets e hilos. A menos que sea AmigaOS
      La única razón para liberar memoria es reutilizarla para otras asignaciones en aplicaciones de larga ejecución sin pedir más memoria nueva al sistema operativo. En herramientas de línea de comandos que se ejecutan una vez y terminan, normalmente no hace falta
    • El sistema operativo sabe que solo puede liberar memoria cuando el proceso termina. Lo mismo aplica a descriptores de archivo y otros recursos
      Si el proceso está diseñado para terminar cuando acaba su trabajo, puedes usar el sistema operativo como si fuera un recolector de basura
      Pero nunca ha existido una forma en que el sistema operativo sepa qué memoria ya no se usa dentro de un programa en ejecución. Quizá algún sistema operativo de investigación particularmente esotérico sea una excepción. Así que más que una oportunidad perdida, creo que aquello que supuestamente se perdió no existe de una forma significativa
      En cambio, un estilo de programación que usa programas muy simples y de vida corta es completamente válido. Las herramientas CLI y los lenguajes de scripting que las automatizan funcionan así, y en el pasado los servidores web también funcionaban así con CGI y similares; incluso hoy sigue siendo un enfoque perfectamente razonable
    • Epsilon GC de Java es exactamente eso
  • (1) La transición de RCU a una recolección de basura por rastreo de propósito general se siente como un cambio de tema con trampa
    (2) La administración manual de memoria no es solo llamar a malloc/free, también trata de la disposición en memoria. Por ejemplo, separación de arreglos de structs, inlining, offsets implícitos, packing, etc.

    • Sobre (2), Virgil tiene varias funciones para controlar la disposición en memoria a distintos niveles
      Parece que se refería a “arreglo de structs”, y eso puede hacerse con arreglos de tuplas, que según el destino se aplanan/normalizan de forma natural. Es decir, en destinos nativos se convierten en arreglos de structs
      También se puede definir una disposición exacta a nivel de bytes[1], y se usa principalmente para interoperar con otro software o para parsear formatos binarios. Se pueden desempaquetar tipos de datos algebraicos y pronto también se podrá controlar hasta su codificación exacta
      Virgil usa recolección de basura
      [1] https://github.com/titzer/virgil/blob/master/doc/tutorial/La...
    • No estoy de acuerdo con que el punto 2 sea administración manual de memoria
      Es cierto que en los lenguajes modernos administrados falta control sobre la disposición en memoria, pero incluso en los lenguajes de bajo nivel eso está lejos de ser perfecto, y claramente sí hay formas de influir en ello
  • Una cosa que falta en este artículo es que async/await combina muy bien con la recolección de basura
    Personalmente no me gusta async/await por razones estilísticas algo particulares, pero no me voy a extender
    Lo he usado mucho en TypeScript/JavaScript y también en Dart, donde funciona como cabría esperar
    También lo he usado en Rust y, en mi opinión, es un desastre. Forzar el tipo de administración de memoria necesaria para usar async/await en un runtime multihilo es un infierno
    https://doc.rust-lang.org/std/pin/index.html

    • En el código asíncrono común de Rust no hace falta pinning. Eso es para quienes escriben bibliotecas
  • Hay un punto que falta en muchos textos que defienden la recolección de basura, y este artículo también parece caer en eso: la memoria es solo un tipo de recurso
    En programación de sistemas, el código correcto también debe gestionar recursos externos como file handles, sockets, etc. La recolección de basura solo resuelve la parte de la memoria de la aplicación, así que no ayuda en absoluto con este tipo de recursos externos. De hecho, puede volverlo mucho más complejo, y basta con ver qué se necesita para implementar correctamente un IDisposable no trivial en .NET
    En mi experiencia, enfoques como RAII o el conteo de referencias hacen mucho más fácil tratar la memoria y los recursos externos de forma unificada, lo que también facilita escribir código correcto y razonar sobre él
    Eso no significa que me oponga abiertamente a la recolección de basura. Como todo lo demás, es una herramienta con ventajas y desventajas. El enfoque de RCU de “GC manual” que menciona el artículo es interesante para ciertas tareas

    • Hay una gran diferencia entre la memoria y otros recursos. La memoria, al igual que la capacidad de procesamiento, es un elemento fundamental de todo cálculo
      No es casualidad que la mayoría de los modelos teóricos de computación asuman memoria infinita. En algunos tipos de software, como los kernels de sistemas operativos o las aplicaciones hard real-time, también se asigna manualmente la capacidad de procesamiento, pero casi no existen lenguajes que exijan asignarla manualmente
      Por razones similares, la gestión automática de memoria es muy útil para abstraer la computación. Evita que los detalles de memoria de una subrutina se filtren hacia quien la llama, y en el uso de CPU ese tipo de detalles también rara vez se expone
      Todo cálculo no trivial incluye cierta cantidad no constante de procesamiento y memoria, pero la E/S suele ocurrir en los bordes del sistema. La gestión de E/S, por supuesto, también es muy importante, pero en cuanto a la centralidad del concepto de computación y de las abstracciones computacionales, no es tan fundamental como el procesamiento y la memoria
    • Correcto, y el argumento de la seguridad de memoria también se aplica a otros recursos
      Por ejemplo, Rust finalmente obtuvo seguridad de E/S, y por eso file handles como OwnedFd en Unix o handles como OwnedHandle en Windows pasan a ser objetos con propiedad en vez de enteros como el número 4
      En la superficie, esto puede parecer solo una forma de evitar errores tontos, como hacer aritmética con handles o usar mal valores reservados como centinelas, pero gracias al modelo de ownership, incluso cuando se hacen operaciones complicadas con handles, la propiedad queda explícita y también resulta transparente para quienes mantengan el código después
    • Como alguien que pasó sobre todo de C++ a C#, me gusta la gestión de memoria con recolección de basura, pero no me gusta tener que rastrear file handles, sockets, etc.
      Realmente sentí el gran valor de RAII
    • Un punto que suele pasarse por alto al criticar los lenguajes con recolección de basura es que la mayoría de ellos sí tienen mecanismos para la gestión determinística de recursos, pero mucha gente no los aprende
      Algunos lenguajes tienen RAII, otros ofrecen keywords, y otros proporcionan administración tipo arena o lambdas con gestión implícita. Algunos reciben un poco de ayuda del sistema de tipos, y otros mezclan un poco de todo lo anterior
      Además, así como los desarrolladores de sistemas deben depender de analizadores estáticos, los analizadores estáticos de esos lenguajes también pueden verificar lo que se pasó por alto cuando el sistema de tipos por sí solo no basta
    • RAII es excelente, por supuesto, pero un lenguaje con recolección de basura y manejo correcto de excepciones también puede tratar los recursos de forma segura
      Por ejemplo, la sentencia try-with-resources de Java garantiza que los recursos se liberen de forma segura incluso si ocurre una excepción: https://docs.oracle.com/javase/tutorial/essential/exceptions...
      Incluso con estos componentes básicos se puede construir un sistema bastante sólido y seguro en cuanto a recursos