Pandas: Problemas de GC cíclicos

Creado en 8 ene. 2013  ·  14Comentarios  ·  Fuente: pandas-dev/pandas

Un misterio que se depurará pronto:

import pandas as pd
import numpy as np

arr = np.random.randn(100000, 5)

def leak():
    for i in xrange(10000):
        df = pd.DataFrame(arr.copy())
        result = df.xs(1000)
        # result = df.ix[5000]

if __name__ == '__main__':
    leak()
Bug

Comentario más útil

Para el registro, nosotros (+ @ sbneto) estamos usando esto en preducción por un tiempo, y lo está haciendo muy bien:

# monkeypatches.py

# Solving memory leak problem in pandas
# https://github.com/pandas-dev/pandas/issues/2659#issuecomment-12021083
import pandas as pd
from ctypes import cdll, CDLL
try:
    cdll.LoadLibrary("libc.so.6")
    libc = CDLL("libc.so.6")
    libc.malloc_trim(0)
except (OSError, AttributeError):
    libc = None

__old_del = getattr(pd.DataFrame, '__del__', None)

def __new_del(self):
    if __old_del:
        __old_del(self)
    libc.malloc_trim(0)

if libc:
    print('Applying monkeypatch for pd.DataFrame.__del__', file=sys.stderr)
    pd.DataFrame.__del__ = __new_del
else:
    print('Skipping monkeypatch for pd.DataFrame.__del__: libc or malloc_trim() not found', file=sys.stderr)

Todos 14 comentarios

Ok, esto es, en una palabra, jodido. Si agrego gc.collect a ese bucle for, deja de perder memoria:

import pandas as pd
import numpy as np
import gc

arr = np.random.randn(100000, 5)

def leak():
    pd.util.testing.set_trace()
    for i in xrange(10000):
        df = pd.DataFrame(arr.copy())
        result = df.xs(1000)
        gc.collect()
        # result = df.ix[5000]

if __name__ == '__main__':
    leak()

Aquí hay objetos que solo se recolectan cuando se ejecuta el GC cíclico. ¿Cuál es la solución aquí, romper el ciclo explícitamente en __del__ para que el asignador de memoria de Python deje de fastidiarnos?

Puedes probar esto:

from ctypes import cdll, CDLL

import pandas as pd
import numpy as np

arr = np.random.randn(100000, 5)

cdll.LoadLibrary("libc.so.6")
libc = CDLL("libc.so.6")

def leak():
    for i in xrange(10000):
        libc.malloc_trim(0)
        df = pd.DataFrame(arr.copy())
        result = df.xs(1000)
        # result = df.ix[5000]

if __name__ == '__main__':
    leak()

Sospecho que esto no tiene nada que ver con Python, pero eso lo confirmaría.

Sí, eso pareció funcionar. Uso de memoria 450 MB después de ejecutar eso en IPython, luego malloc_trim liberó 400 MB. Muy pernicioso

Siguiendo la ventaja de malloc_trim upstream, esto parece una optimización de glibc que salió mal.
xref:
http://sourceware.org/bugzilla/show_bug.cgi?id=14827

ver el comentario "fastbins".

In [1]: from ctypes import Structure,c_int,cdll,CDLL
   ...: class MallInfo(Structure):   
   ...:     _fields_ =[
   ...:               ( 'arena',c_int ),  #  /* Non-mmapped space allocated (bytes) */
   ...:            ('ordblks',c_int  ),# /* Number of free chunks */
   ...:            (    'smblks',c_int ),  # /* Number of free fastbin blocks */
   ...:            (    'hblks',c_int  ),  #/* Number of mmapped regions */
   ...:            (    'hblkhd' ,c_int ), #/* Space allocated in mmapped regions (bytes) */
   ...:            (    'usmblks' ,c_int), # /* Maximum total allocated space (bytes) */
   ...:            (    'fsmblks' ,c_int) ,#/* Space in freed fastbin blocks (bytes) */
   ...:            (    'uordblks' ,c_int),# /* Total allocated space (bytes) */
   ...:            (    'fordblks',c_int ),# /* Total free space (bytes) */
   ...:            (    'keepcost',c_int )# /* Top-most, releasable space (bytes) */
   ...:          ]
   ...:     def __repr__(self):
   ...:         return "\n".join(["%s:%d" % (k,getattr(self,k)) for k,v in self._fields_])
   ...: 
   ...: cdll.LoadLibrary("libc.so.6")
   ...: libc = CDLL("libc.so.6")
   ...: mallinfo=libc.mallinfo
   ...: mallinfo.restype=MallInfo
   ...: libc.malloc_trim(0)
   ...: mallinfo().fsmblks
Out[1]: 0

In [2]: import numpy as np
   ...: import pandas as pd
   ...: arr = np.random.randn(100000, 5)
   ...: def leak():
   ...:     for i in xrange(10000):
   ...:         df = pd.DataFrame(arr.copy())
   ...:         result = df.xs(1000)
   ...: leak()
   ...: mallinfo().fsmblks
Out[2]: 128

In [3]: libc.malloc_trim(0)
   ...: mallinfo().fsmblks
Out[3]: 0

Entonces no se arreglará. Tal vez deberíamos agregar algunas funciones de ayuda a los pandas algún día para hacer el recorte de malloc

¿Entrada en FAQ, tal vez?

Para el registro, nosotros (+ @ sbneto) estamos usando esto en preducción por un tiempo, y lo está haciendo muy bien:

# monkeypatches.py

# Solving memory leak problem in pandas
# https://github.com/pandas-dev/pandas/issues/2659#issuecomment-12021083
import pandas as pd
from ctypes import cdll, CDLL
try:
    cdll.LoadLibrary("libc.so.6")
    libc = CDLL("libc.so.6")
    libc.malloc_trim(0)
except (OSError, AttributeError):
    libc = None

__old_del = getattr(pd.DataFrame, '__del__', None)

def __new_del(self):
    if __old_del:
        __old_del(self)
    libc.malloc_trim(0)

if libc:
    print('Applying monkeypatch for pd.DataFrame.__del__', file=sys.stderr)
    pd.DataFrame.__del__ = __new_del
else:
    print('Skipping monkeypatch for pd.DataFrame.__del__: libc or malloc_trim() not found', file=sys.stderr)

@alanjds muchas gracias!

Pero hay otras operaciones afectadas :-(

Es MUY extraño que el problema anterior (problema de glibc) no tenga ninguna reacción. Afecta a TODO el entorno de las PC y servidores Linux. ¡¡¡Y nada!!!

Lo sé, me dirás: ¡ok, escribe un parche! Lo haré (UPD: pero será extraño porque no sé nada sobre el código glibc). Pero ni siquiera nadie lo sabe.

Todo el mundo dice: KDE se filtra. ¿Quién sabe, por qué? ¡Nadie!

¿Fuente abierta? ¡Para vergüenza! Lo siento, pero es cierto para esta situación.

PD http://sourceware.org/bugzilla/show_bug.cgi?id=14827

Yo creo en ti 2 años y ningún movimiento de ese lado: /

Digo que se arregle de este lado y ponga un gran comentario de culpa, porque bifurcar allí parece inviable.

@alanjds Tu código solucionó un problema que me estaba causando un gran dolor de cabeza. ¿Estaría dispuesto a explicar cuál es el comportamiento predeterminado de los pandas y cómo lo corrige su código?

También puede solucionar este problema cambiando a jemalloc como asignador predeterminado. En lugar de python script.py , ejecute LD_PRELOAD=/usr/lib/libjemalloc.so python script.py . Tenga en cuenta que la ruta a libjemalloc.so puede ser diferente en su sistema y que primero debe instalarla con su administrador de paquetes.

@tchristensenowlet El problema parece estar en el código malloc de glibc . Aparentemente, la implementación free allí no respeta una bandera que debería emitir malloc_trim después de cierto umbral, como puede ver en el enlace de @ghost . Por lo tanto, malloc_trim nunca se llama y la memoria se pierde. Lo que hicimos fue simplemente llamar manualmente malloc_trim si la biblioteca está disponible en el sistema. Lo llamamos en el método __del__() , que se ejecuta cuando el objeto se recolecta como basura.

glibc.malloc.mxfast tunable se ha introducido en Glibc (https://www.gnu.org/software/libc/manual/html_node/Memory-Allocation-Tunables.html).

Creo que este podría ser el culpable de uno de nuestros proyectos, pero nuestros usuarios están ejecutando Windows con Python 3.8 predeterminado (del sitio web oficial) y con todas las dependencias instaladas a través de pip. ¿Este problema también estaría en Windows? Si es así, ¿cuál sería el cdll.LoadLibrary("libc.so.6") ?

Editar: ejecuté estas pruebas descritas aquí, y la basura recolectada hizo su trabajo correctamente cada vez:
https://github.com/pandas-dev/pandas/issues/21353
Sistema: Windows 10
Python: 3.8.5
Pandas: 1.1.0

¿Fue útil esta página
1 / 5 - 1 calificaciones