collectgarbage: medir, forzar, detener y afinar sin romper nada
Una sola función de la biblioteca básica concentra todo el control que Lua te da sobre el recolector: contar la memoria viva, forzar un ciclo completo, dar un paso suelto, detener y reanudar la recolección automática y ajustar los parámetros que gobiernan la frecuencia y el tamaño del trabajo. Lua 5.5 reorganizó esa última parte con una opción dedicada a parámetros. Esta lección recorre cada opción, explica qué mide de verdad cada número y dedica su última sección a lo más importante: cuándo tocar esto es profesional y cuándo es un error de principiante disfrazado de optimización.
El recolector de Lua expone una superficie de control minúscula: una función, un puñado de cadenas como opción y unos pocos números. Esa parquedad es deliberada y es coherente con todo lo demás en el lenguaje —no hay perillas para todo, hay las justas— pero tiene un efecto secundario incómodo: como la interfaz es pequeña, parece inofensiva, y mucha gente la usa como si lo fuera. La realidad es que dos de esas opciones sirven para diagnosticar, una sirve para medir, y las demás son bisturíes que en manos equivocadas convierten un programa con un perfil de memoria razonable en uno que se traga la RAM o que se atasca en el peor momento posible. Esta lección enseña las siete opciones y después, con más énfasis, enseña a no usarlas.
- Interpretar correctamente qué mide
collectgarbagecon la opción de conteo y qué no mide en absoluto. - Distinguir un ciclo completo forzado de un paso suelto, y saber qué devuelve cada uno.
- Detener y reanudar el recolector con garantías, y comprobar si está corriendo.
- Enumerar los parámetros de cada modo y decidir en qué situaciones ajustarlos está justificado.
Medir: la opción que sí deberías usar
La opción de conteo devuelve los kilobytes de memoria que Lua tiene en uso, con parte fraccionaria, de modo que multiplicando por 1024 obtienes el número exacto de bytes. Es la única forma que ofrece el lenguaje de mirar dentro de su propio consumo, y es la opción con la que más tiempo deberías pasar.
local antes = collectgarbage("count")
local datos = {}
for i = 1, 100000 do datos[i] = { id = i } end
local despues = collectgarbage("count")
print(string.format("%.1f KB para 100000 tablas", despues - antes))
Hay dos advertencias que separan una medida útil de una cifra sin sentido. La primera es que ese número incluye la basura todavía no recogida, así que para medir lo que de verdad está vivo hay que forzar un ciclo completo antes de leer. La segunda es que cuenta solo la memoria gestionada por Lua: no incluye lo que reservan las bibliotecas de C con las que trabajas, ni el propio ejecutable, ni la fragmentación del asignador del sistema. Comparar esta cifra con la memoria residente que reporta el sistema operativo y esperar que coincidan es un error clásico; nunca coinciden, y la diferencia puede ser enorme.
-- Medida honesta de memoria viva: recoger primero, contar despues.
local function memoria_viva()
collectgarbage("collect")
collectgarbage("collect") -- la segunda limpia lo resucitado por finalizadores
return collectgarbage("count")
end
La segunda llamada no es superstición. Un finalizador puede resucitar objetos y crear basura nueva, así que la primera recolección deja residuos que la segunda barre. Si tu programa usa __gc, cuenta dos veces.
Forzar, dar un paso, parar y reanudar
Las opciones de control son cuatro y conviene tener muy claro qué hace cada una.
| Opción | Qué hace | Qué devuelve |
|---|---|---|
"collect" |
Ejecuta un ciclo completo, de una pieza. Es el modo por defecto si no pasas nada | Cero |
"step" |
Avanza el recolector. Con un argumento nulo hace un paso básico indivisible; con un número, actúa como si se hubieran asignado esos kilobytes | Verdadero si con ese paso terminó un ciclo |
"stop" |
Detiene la recolección automática. Los pasos manuales siguen funcionando | Cero |
"restart" |
Reanuda la recolección automática | Cero |
Existe además una opción para preguntar si el recolector está corriendo, que devuelve un valor booleano y es lo que debes usar antes de suponer nada sobre el estado.
if not collectgarbage("isrunning") then
collectgarbage("restart")
end
El detalle importante de "stop" es que no impide que la memoria se libere manualmente: con el recolector detenido puedes seguir dando pasos explícitos, y ese es exactamente el patrón que justifica su existencia. Detener la recolección automática durante una sección crítica y hacer los pasos tú, en el momento del bucle en que hay holgura, es una técnica legítima y muy usada en motores de juego.
-- Patron valido: el programa decide cuando se recolecta.
collectgarbage("stop")
while corriendo do
procesar_entrada()
actualizar_mundo()
dibujar()
-- En el hueco entre imagenes, un mordisco controlado:
local sobra = presupuesto_restante_ms()
if sobra > 2 then collectgarbage("step", 8) end
end
Un "stop" sin una política explícita de pasos manuales convierte a Lua en un lenguaje sin recolector: la memoria crece de forma monótona hasta que el proceso muere. Si detienes el recolector, la responsabilidad de avanzarlo pasa a ser tuya, y esa responsabilidad hay que escribirla en el mismo módulo, no dejarla en un comentario.
Los parámetros, y cómo cambió su interfaz en 5.5
Cada modo tiene sus propias perillas. En el modo incremental son tres: la pausa, que decide cuánto tiene que crecer la memoria respecto al final del ciclo anterior antes de empezar uno nuevo —un valor de doscientos significa esperar a que se duplique—; el multiplicador de paso, que fija la velocidad del recolector en relación con la del programa que asigna; y el tamaño de paso, que determina de qué tamaño es cada mordisco individual.
En el modo generacional las perillas responden a las tres preguntas de la lección anterior: cada cuánto se convoca una recolección menor, cuándo se decide escalar a una mayor y cuándo se vuelve al régimen de menores tras una mayor. Sus nombres son minormul, minormajor y majorminor.
Lua 5.5 reorganizó la interfaz. Hasta 5.4, los parámetros se pasaban como argumentos extra al activar el modo. Desde 5.5 hay una opción dedicada que consulta o fija un parámetro por nombre y devuelve el valor anterior, lo cual es mucho más manejable, permite tocar una sola perilla sin repetir las demás y hace trivial restaurar el estado original.
-- Lua 5.5: un parametro por nombre, devuelve el valor previo.
local pausa_original = collectgarbage("param", "pause")
collectgarbage("param", "pause", 400) -- esperar a cuadruplicar la memoria
-- ... seccion que asigna mucho y libera casi todo ...
collectgarbage("param", "pause", pausa_original) -- restaurar siempre
-- Forma antigua, valida hasta 5.4: parametros como argumentos del modo.
collectgarbage("incremental", 200, 100, 13)
collectgarbage("generational", 20, 100)
Guardar y restaurar no es una formalidad. Un parámetro global tocado en un módulo y no restaurado afecta a todo el programa, incluidas bibliotecas de terceros que nunca aceptaron ese trato. Si ajustas, ajusta con alcance acotado.
Contar exige recoger antes
Una medida de memoria sin un ciclo completo previo incluye basura y no significa nada. Y si hay finalizadores, hacen falta dos ciclos para que la cifra sea estable.
Cuenta solo lo de Lua
Los bloques reservados por bibliotecas de C, los buffers del sistema y la fragmentación del asignador quedan fuera. La memoria residente del proceso siempre será mayor.
Subir la pausa cambia memoria por tiempo
Duplicar la pausa recolecta la mitad de veces y consume bastante más memoria pico. Es un intercambio explícito, no una mejora, y solo compensa si la memoria sobra de verdad.
Restaura lo que tocas
Los parámetros son estado global del intérprete. Guarda el valor previo, envuélvelo en un bloque acotado y devuélvelo, o acabarás depurando el recolector de otra persona.
Cuándo tocarlo y cuándo es un error
Hay tres usos de esta función que son profesionales y no admiten discusión. Medir durante el desarrollo, para saber qué cuesta cada estructura. Forzar un ciclo completo en pruebas, para comprobar que un objeto se libera de verdad o para que corran los finalizadores de forma determinista. Y coordinar la recolección con el ritmo de la aplicación cuando tu programa tiene un bucle con huecos conocidos, deteniendo la recolección automática y dando pasos donde sabes que hay holgura.
Y hay tres que casi siempre son un error. El primero es forzar un ciclo completo en el camino caliente, típicamente una llamada por imagen o por petición: eso sustituye una heurística ajustada por la pausa más larga que el recolector puede producir, repetida a propósito, y es la forma más rápida que existe de arruinar la latencia de un programa Lua. El segundo es detener el recolector para ir más rápido sin ninguna política de pasos manuales; funciona de maravilla en el banco de pruebas de diez segundos y revienta en producción a las tres horas. El tercero, el más frecuente, es tocar los parámetros antes de haber medido: los valores por defecto llevan décadas afinándose contra programas reales, y la probabilidad de que un número elegido a ojo mejore tu caso concreto sin empeorar otro es muy baja.
La regla que resume todo esto es que ajustar el recolector es la última optimización, no la primera. Antes de mover una perilla hay que haber probado lo que de verdad funciona: crear menos objetos en los bucles calientes, reutilizar tablas en lugar de reconstruirlas, preasignar las que se conocen de antemano, evitar concatenaciones repetidas y no meter objetos frescos dentro de estructuras viejas más de lo necesario. Todo eso reduce el trabajo del recolector en origen, y ninguna configuración compensa un programa que genera basura a espuertas.
La tentación de configurar el recolector nace de un error de modelo mental que conviene desmontar de raíz: creer que el recolector es un subsistema independiente cuyo comportamiento se puede ajustar desde fuera, cuando en realidad es un espejo del programa. La frecuencia de los ciclos la fija la velocidad a la que asignas; el coste de cada ciclo lo fija el tamaño de tu grafo vivo; el coste de las barreras lo fija cuántas referencias nuevas escribes dentro de estructuras viejas. Los tres números que puedes tocar no cambian ninguna de esas magnitudes, solo redistribuyen sus consecuencias en el tiempo. Por eso el resultado típico de una sesión de ajuste hecha sin medir es un programa que va igual de lento y además consume el doble de memoria: has movido el problema de sitio y has perdido los valores por defecto, que eran razonables. La forma productiva de usar collectgarbage es exactamente la inversa a la que sugiere su nombre: no como un mando de control, sino como un instrumento de diagnóstico. Cuando la memoria crece de forma monótona, la opción de conteo te dice si el problema es tuyo o del recolector, y la respuesta es siempre que es tuyo —hay algo alcanzable que no debería estarlo, como viste en la primera lección—. Cuando la latencia tiene picos, forzar ciclos completos y medirlos te dice cuánto cuesta tu grafo vivo, y si esa cifra te asusta, la solución no es un parámetro sino tener menos objetos vivos. Cuando los finalizadores no corren, forzar dos ciclos te lo confirma en un segundo. En los tres casos la función te ha dado información que no tenías, y en ninguno la solución ha sido girar una perilla. El único ajuste que sobrevive de verdad al contacto con la producción es el patrón de detener la recolección automática y dar pasos donde tú sabes que hay holgura, y sobrevive precisamente porque no es un ajuste: es una política, escrita en tu código, visible en la revisión y ligada al ritmo real de tu aplicación, que es información que el recolector nunca podrá deducir por su cuenta.
La opción de conteo devuelve kilobytes de memoria gestionada por Lua, con parte fraccionaria, y solo significa algo tras un ciclo completo —dos si hay finalizadores—. La opción de ciclo completo recolecta de una pieza; la de paso avanza un mordisco y devuelve verdadero si terminó el ciclo. Detener y reanudar controlan la recolección automática, y detener sin dar pasos manuales es una fuga programada. Los parámetros son pausa, multiplicador y tamaño de paso en modo incremental, y los tres umbrales de menores y mayores en generacional; desde Lua 5.5 se consultan y fijan uno a uno por nombre. Medir y forzar en pruebas es profesional; forzar en el camino caliente y ajustar sin medir, no.
- Mide con la opción de conteo cuánto ocupan exactamente una tabla vacía, una tabla con diez campos con nombre y una secuencia de diez enteros. Explica las tres cifras.
- Escribe la función de memoria viva de la lección y demuestra con un finalizador que la segunda recolección es necesaria.
- Detén el recolector, asigna en bucle registrando el conteo cada mil iteraciones y representa la curva. Reanuda y observa dónde se desploma.
- Implementa el patrón de pasos con presupuesto del ejemplo del bucle de imagen y compara el histograma de latencias contra el mismo bucle con recolección automática.
- Duplica el parámetro de pausa en un programa que asigne mucho, mide memoria pico y tiempo total, y decide con datos si el intercambio te compensa. Restaura el valor original al terminar.