From gc import commonsense - garbage collection

../images/commonsense_gc/claudiofreire.jpg
Autor:Claudio Freire
Bio:Python, C, C++, Java, assembler, código máquina, lo que quieras.
Email:freireclaudio@yahoo.com.ar

Nadie piensa en cómo se administra la memoria en python. Es una de sus magias más queridas. Simplemente anda. Pero la verdad es que, cuando se pone presión sobre el colector de basura (garbage collector para los que saben) de Python, se vislumbran sus muchas limitaciones. Es imprescindible conocerlas para no encontrarse con problemas inesperados en nuestros programas.

Algo de contexto

Garbage collection, o recolección de basura, es cualquier método para detectar y liberar memoria no referenciada en un programa de manera automática. Hay muchos métodos de recolección de basura, aunque en general el término se asocia a los métodos de seguimiento de referencias utilizados por java, que siguen las cadenas de referencias desde las variables del programa para liberar cualquier objeto no alcanzable desde ellas.

Estos métodos tienen la ventaja de ser "a prueba de fallas", en el sentido que son siempre efectivos en encontrar objetos que ya no son útiles para el programa. Pero carecen de inmediatez, pues deben realizar un análisis de todos los objetos en el sistema antes de poder detectarlos, y carecen de semánticas de finalización claras e inambiguas.

En Python, estos métodos no son aplicables. Debido a la arquitectura del código C de Python, no es posible saber todas las variables del programa pues, además de las variables que mantiene el intérprete, las bibliotecas de extensión pueden mantener referencias en la pila de C, que yace en el hardware y es difícil de explorar portable y eficientemente.

Desde su nacimiento, Python utilizó otro método, que es el conteo de referencias. A pesar de no llamarse "recolección de basura" en la literatura, es también un método de recolección de basura. Pero no es completo: el método falla si hay referencias cíclicas. Un ejemplo sencillo de referencia cícilica es un objeto que se referencia a sí mismo, como:

l = []
l.append(l)
del l

En el código de arriba, la lista nunca será liberada. El conteo de referencias funciona manteniendo un contador con la cantidad de punteros que apuntan a un objeto. Cuando la lista se crea, el contador es 1 (por la variable "l" que apunta al objeto). Al agregarse la lista a sí misma, el contador se vuelve 2 (porque la lista tiene internamente otro puntero que apunta a ella). Al limpiarse la variable (con "del l"), el contador vuelve a 1, pues la variable "l" ya no apunta a la lista, pero el puntero que la lista tiene apuntando a sí misma aún existe y cuenta como una referencia. El contador nunca volverá a cero, y por tanto la lista nunca será liberada.

En un entorno con conteo de referencias, estos ciclos se consideran mala práctica y, de hecho, casi siempre son evitables. El problema es que en programas complejos, a veces es difícil evitarlos, resulta en código antinatural, o incluso el programador puede no darse cuenta que está introduciendo ciclos, pues no siempre son tan directos como en el ejemplo. Un ejemplo más retorcido sería:

class SomeGarbage:
    def __init__(self):
        try:
            self.dosomething()
        except:
            self.error = sys.exc_info()

Muchos frameworks hacen este tipo de cosas, para poder reportar correctamente errores que surgen de improvisto sin romper la ejecución del programa. El tema es que exc_info incluye tracebacks, y los tracebacks incluyen referencias a todas las variables de la pila en toda la pila de llamadas. Esto incluye el self de SomeGarbage.__init__, y probablemente otras variables de código que llamó al SomeGarbage. En síntesis, el ciclo puede venir de código remoto y desconocido para los programadores. Es a veces difícil eliminarlos si no son tan evidentes.

Por eso, para Python se introdujo otro tipo de colector de basura, uno que opera sólo sobre los ciclos (que es el punto débil del conteo de referencias). Con este parche, introducido activado por defecto desde Python 2.0, un tiempo atrás, los programadores casi no tienen que preocuparse por nada. Casi.

Colector de ciclos

El algoritmo de Boehm-Demers es uno de los algoritmos de marcado y barrido (mark and sweep, como el que usa Java) más usados. El algoritmo primero marca todos los objetos como potencialmente no alcanzables, y recorre las referencias en la pila y las variables locales, desmarcando objetos a medida que encuentra referencias.

En el caso de python, este algoritmo se aplica únicamente a los ciclos. El objetivo es, pues, complementar al contador de referencias, detectando el tipo de basura que el conteo no puede detectar. Esto se llama pues colección de ciclos.

Como el contador de referencias puede manejar perfectamente todos los casos que no generan ciclos, esto elimina efectivamente cualquier objeto que no pueda contener referencias a otros objetos. Esto incluye multitud de tipos de objetos muy usados en python: números, cadenas, unos cuantos tipos de la biblioteca estándar, como slices, xrange, etc... todos tipos que deben ser pequeños y rápidos, siguen siéndolo si se ignoran en el colector de ciclos.

El colector de ciclos trabaja de forma inversa al colector de basura, pero utiliza el mismo algoritmo de Boehm-Demers, con pocas modificaciones. Tomando todos los objetos contenedores (es decir, listas, diccionarios, tuplas, objetos con atributos, etc), el algoritmo detecta los objetos que tienen referencias fuera de este conjunto. Esto es, referencias desde el intérprete o desde bibliotecas de extensión. Así que los objetos referenciados desde no contenedores, o alcanzables desde no contenedores, se consideran vivos. Todo objeto que sólo sea alcanzable desde contenedores y no desde afuera, debe ser, pues, basura.

Efectivamente, este método detecta todos los objetos python inútiles. Pero no siempre puede liberarlos.

Finalización de ciclos

Cuando los ciclos contienen finalizadores (__del__), surge un problema: no hay manera obvia de finalizarlos. Si dos objetos en un ciclo tienen finalizadores, cualquier orden de destrucción puede traer problemas. Si A se destruye primero, B podría intentar accederlo luego, o viceversa. Incluso si se llamara al finalizador sin destruir el objeto, los objetos pueden no estar preparados para el subsiguiente acceso puesto que se supone que el finalizador es lo último que se hace con un objeto.

En síntesis, es un caso imposible, y Python optó por considerarlos "Basura no colectable".

La basura no colectable es un problema eterno para los programadores de python, difícil de diagnosticar y de solucionar una vez diagnosticado. Son objetos que python no puede liberar, y como quedaron fuera del alcance del programa, el programa tampoco puede hacerlo. Es, efectivamente, la única forma de memory-leak en python. En python puro, al menos.

Generaciones, guarderías y casas de retiro.

Como el análisis este es complejo y requiere recursos, surgió el conecpto de guarderías y generaciones. Las generaciones son grupos de objetos que han vivido por cierta cantidad de tiempo. En general, hay mínimo tres generaciones: la guardería, la generación donde nacen todos los objetos, la generación intermedia, y la generación permanente donde están los objetos de larga vida.

Las generaciones existen para acelerar la recolección de basura. La guardería es donde están los objetos de muy corta duración, se recolecta seguido, y suele ser pequeña. Cuando los objetos sobreviven una recolección en la guardería, se mueven a la generación intermedia, que se revisa menos seguido. A medida que van sobreviviendo, van moviéndose de generación en generación, hasta que llegan a la última generación, la generación permanente, que se limpia muy poco seguido.

Python tiene normalmente sólo tres generaciones, y la frecuencia de recolección está dada por una métrica poco común: la cantidad de creaciones menos destrucciones. La razón de usar esta métrica es para evitar disparar recolecciones cuando el conteo de referencia funciona bien. Cuando no funciona, se presume, habrán muchas creaciones pero pocas destrucciones. Un efecto indeseable es que, a veces, esta heurística no funciona, y un par de objetos en un ciclo pequeño puede que consuman mucha memoria, no disparen la recolección, y la memoria no se libere a tiempo.

Defragmentación

Algunos recolectores de basura, como el de java, no sólo liberan objetos inservibles. También mueven los objetos que sí sirven, para asegurarse que la memoria usada es bien compacta. La utilidad de esto se deriva del hecho que el sistema operativo opera en páginas y segmentos, la memoria libre en el medio de una página o segmento no está libre para otros programas, sólo para Python.

Como las bibliotecas de extensión están hechas en C (de hecho, python también), y usan punteros directos a los objetos, Python no puede defragmentar la memoria. Si moviera un objeto de lugar, no tiene forma de actualizar todos los punteros que hacen referencia a ese objeto.

Por lo tanto, la recolección temprana de los ciclos es más importante en Python que en otros lenguajes con recolectocción de basura, pues evita la fragmentación de la memoria. Con esto en mente, a veces, luego de operaciones complejas que sabemos que generan mucha basura, puede ser conveniente llamar explícitamente a gc.collect. Lo que nos lleva a:

from gc import glory

El colector de ciclos de python viene con un módulo, el módulo gc, que nos permite tunear, depurar, e incluso forzar una recolección.

Una herramienta indispensable para la depuración es gc.garbage, una lista que contiene todos la basura no recolectable (o, directamente, toda la basura si se pone al módulo en modo depuración).

>>> import gc,sys
>>> class SomeGarbage:
...    def __init__(self):
...        try:
...            self.dosomething()
...        except:
...            self.error = sys.exc_info()
...    def __del__(self):
...        print "me borré"
...    def dosomething(self):
...        raise RuntimeError, "No te creo"
...
>>> g = SomeGarbage()
>>> gc.garbage
[]
>>> del g
>>> gc.garbage
[]
>>> gc.collect()
10
>>> gc.garbage
[<__main__.SomeGarbage instance at 0x7fdfa98f0b90>]
>>>

En este ejemplo vemos cómo los tracebacks generan referencias cíclicas, y al usar __del__ para imprimir cuándo se destruye el objeto, estamos inhibiendo la recolección del ciclo. También es visible el hecho que la recolección no es inmediata, y para el ejemplo tuvimos que forzar la recolección (con gc.collect, muy útil por cierto). También podemos ver que gc.collect recolectó 10 objetos, lo cual ilustra lo común que es generar ciclos, y lo difícil que es vislumbrarlos.

De hecho, podemos ver exactamente dónde está el problema:

>>> g = SomeGarbage()
>>> g.error
(<type 'exceptions.RuntimeError'>, RuntimeError('No te creo',), <traceback object at 0x7fdfa98f0d40>)
>>> t = g.error[2]
>>> t.tb_frame
<frame object at 0x793d00>
>>> t.tb_frame.f_locals
{'self': <__main__.SomeGarbage instance at 0x7fdfa98f0c68>}

En modo depuración, gc nos permite ver todos los ciclos en nuestro programa. Honestamente, no lo encontré muy útil en proyectos grandes, llenos de ciclos colectables. Pero, a veces, es el único recurso que queda para depurar.

>>> import gc
>>> gc.set_debug(gc.DEBUG_SAVEALL)
>>> l = []
>>> l.append(l)
>>> del l
>>> gc.collect()
1
>>> gc.garbage
[[[...]]]

Normalmente, la lista no aparecería como garbage. Pero al poner al colector en modo depuración, todos los ciclos son almacenados en gc.garbage, no sólo los no colectables. Es útil cuando nuestro programa debe correr en tiempo real, pues la recolección de ciclos introduce pequeñas pausas que pueden no ser aceptables. Con este modo, podemos atrapar los ciclos generados por el programa para ver cómo no generarlos, y con eso evitar las pausas por recolección de ciclos.

Referencias débiles

Una de las maneras más sencillas (y más correctas) de evitar referencias circulares, es mediante la utilización de referencias débiles.

Las referencias débiles, son referencias que no incrementan el contador de referencias. Cuando un objeto es destruido, todas las referencias débiles se trasnforman en None. Es muy fácil utilizar referencias débiles, el módulo weakref nos provee una forma sencillísima de usarlas:

>>> import weakref
>>> a = A()
>>> a.me = weakref.ref(a)
>>> a.me
<weakref at 0x7f203a675b50; to 'instance' at 0x7f203a6a64d0>
>>> a.me()
<__main__.A instance at 0x7f203a6a64d0>
>>> 2 # hace falta, para olvidar a la referencia "_" del intérprete interactivo
2
>>> del a
borróseme
>>>

Este ejemplo ilustra cómo las referencias débiles rompen ciclos, pues pudimos borrar al objeto sin problemas, y lo fácil que es promover una referencia débil a fuerte (llamando a la referencia débil), que es necesario para utilizar el objeto referenciado.

Recetario - Qué evitar

Los siguientes ejemplos muestran formas comunes de generar leaks de memoria. O sea, hay que evitar hacer lo que se hace allí, o, al menos, pensarlo muy bien antes de hacerlo:

Guardar lambdas con self

Esto genera una referencia circular puesto que el objeto lambda contiene una referencia implícita al self. Es especialmente malo si SomeClass tiene __del__. También hay que tener cuidado con otras variables libres que no sean self, conviene pensar cuidadosamente antes de hacer esto.

Evitar:

class SomeClass:
    ...
    def somefunc(self):
        ...
        self.atributo = lambda : self.haceralgo()

No hay problema, sin embargo, si el lambda no se almacena en ningún atributo o lista del objeto. Si sólo se pasa por parámetros, está todo bien. Incluso si se almacena en otros objetos no relacionados (siempre que SomeClass no tenga, incluso indirectamente, referencias al objeto que contenga el lambda).

Guardar tracebacks

Es muy peligroso guardarse tracebacks. Si hace absoluta falta, conviene convertirlos a string, o tener algún método que los borre del objeto cuando ya no hagan falta

Evitar:

class SomeClass:
    ...
    def somefunc(self):
        try:
            ...
        except:
            self.error = sys.exc_info()

Generadores

No es mala idea usarlos, pero hay que tener en cuenta que todas las variables locales del generador quedarán vivas mientras se use el generador. Cuando los generadores tienen variables libres, es exactamente lo mismo que un lambda, y pueden tener referencias a self y otras variables libres. Lo bueno, sólo referencian a variables que hayan sido usadas en el generador, así que es posible usarlos bien

Evitar:

def __init__(self, raw_data):
    self.raw_data = raw_data
    self.data = itertools.cycle(x for x in self.raw_data if x.enabled)

En este ejemplo, sería mejor referenciar directamente a raw_data - o incluso usar @property para definir el atributo data, en vez de almacenar el iterador directamente.

Recetario - Qué usar

Los siguientes ejemplos muestran formas comunes de evitar leaks de memoria.

Guardar lambdas con referencias directas

En vez de acceder a los atributos a través de self, siempre que se pueda, conviene referenciar al valor que tienen directamente. Por supuesto, si es necesario que el lambda referencie al valor actual al momento de ser llamado (y no al momento de ser creado), puede que esta técnica no funcione.

class SomeClass:
    ...
    def somefunc(self):
        ...
        algunvalor = self.algunvalor
        self.atributo = lambda : algunvalor

Guardar tracebacks formateados

Es muy peligroso guardarse tracebacks. Así que si hace falta, conviene guardarlos como strings.

class SomeClass:
    ...
    def somefunc(self):
        try:
            ...
        except:
            self.error = traceback.format_exc()

Generadores

Como con los lambdas, conviene apuntar a valores directamente. O no usarlos.

def __init__(self, raw_data):
    self.raw_data = raw_data
    self.data = itertools.cycle(x for x in raw_data if x.enabled)

O

def __init__(self, raw_data):
    self.raw_data = raw_data
    self.data = itertools.ifilter(operator.attrgetter('enabled'), self.raw_data)

O

def __init__(self, raw_data):
    self.raw_data = raw_data

@property
def data(self):
    raw_data = self.raw_data
    return itertools.cycle(x for x in raw_data if x.enabled)

Si hace falta usar generadores de la manera que generan ciclos, asegurarse que los objetos involucrados no tengan __del__.

Help PET: Donate

blog comments powered by Disqus

Último cambio: Thu Sep 22 06:38:27 2011.  -  Esta revista está bajo una licencia Creative Commons.