Detectar y corregir fugas de memoria correctness en Haskell con STG
Un análisis técnico sobre cómo leer el código intermedio de GHC para identificar fugas de memoria por perezosidad que rompen la lógica del programa, más allá del rendimiento.
Las fugas de espacio en Haskell suelen asociarse a la eficiencia, pero existen casos donde la acumulación de memoria impide que el programa termine o devuelva el resultado correcto. Ese es el problema de las fugas de "correctness": no es que la aplicación vaya lenta, es que simplemente no funciona porque el recolector de basura no libera objetos que ya no deberían existir. Un artículo reciente desgllosa cómo abordar este problema específico utilizando el código STG (STG Intermediate Representation) generado por GHC como modelo operativo para entender exactamente qué está ocurriendo en memoria.
El autor clasifica estas fugas críticas en tres tipos, basándose en cómo interactúan con la evaluación perezosa. Los "strictness leaks" ocurren cuando una función recursiva o un binding en un DSL acumula thunks que dependen unos de otros. La solución clásica, como en el caso de foldl, es forzar la evaluación antes de la llamada recursiva usando seq o su homónimo estricto foldl'. Es un patrón local: el código de la función misma es el culpable y la corrección también es local.
Los "liveness leaks" son más sutiles. Suceden cuando una referencia a un valor grande se mantiene viva porque está contenida en el thunk de una pipeline que aún no se ha forzado. Para el recolector de basura, ese valor sigue siendo necesario hasta que se evalúa la expresión completa. Esto requiere una planificación previa de la estructura del programa para evitar retener referencias innecesarias en cierres que no se evaluarán inmediatamente.
Los "excessive sharing leaks" afectan a generadores compartidos por dos o más pipelines. Si un generador grande se evalúa parcialmente para una de las pipelines, el resultado se materializa en memoria. Si la segunda pipeline no consume esos valores de la misma manera o en la misma cantidad, la memoria permanece ocupada innecesariamente. Aquí, la estructura de datos generada debe diseñarse cuidadosamente para minimizar la retención global.
La herramienta principal para diagnosticar esto no es un perfilador de ejecución, sino la lectura del código STG generado por ghc -c -O1 -ddump-stg-final. Aunque la sintaxis de nombres de variables resulte confusa al principio, el código STG elimina las abstracciones de la perezosidad. Muestra explícitamente todas las asignaciones, los cierres (closures) y las variables libres. Al leerlo, se puede ver exactamente dónde se crean los thunks y cómo fluyen las referencias, permitiendo identificar si una variable se está reteniendo más tiempo del necesario.
El texto original incluye ejemplos concretos, como la implementación del bind del monad Writer estricto, donde se muestra cómo el código STG revela asignaciones innecesarias que el código de alto nivel oculta. La recomendación es usar este modelo intermedio para justificar las correcciones manuales antes de recurrir a herramientas de profiling en tiempo de ejecución. Es un enfoque preventivo: entender la evaluación para escribir código que no acumule estado oculto.

