Pandas: Problemas cíclicos de GC

Criado em 8 jan. 2013  ·  14Comentários  ·  Fonte: pandas-dev/pandas

Um mistério a ser depurado em breve:

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

Comentários muito úteis

Para registro, nós (+ @ sbneto) estamos usando isso na predução por um certo tempo, e está indo muito bem:

# 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 comentários

Ok, isso é, em uma palavra, f * cked. Se eu adicionar gc.collect a esse loop for, ele para de vazar memória:

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()

Existem objetos aqui que só são coletados quando o GC cíclico é executado. Qual é a solução aqui, interrompa o ciclo explicitamente em __del__ para que o alocador de memória Python pare de nos ferrar?

Você pode tentar isto:

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()

Suspeito que isso não tenha nada a ver com python, mas isso confirmaria.

Sim, isso parecia funcionar. Uso de memória 450 MB após executá-lo no IPython, então malloc_trim liberou 400 MB. Muito pernicioso

Seguindo a liderança malloc_trim upstream, isso parece uma otimização glibc que deu errado.
xref:
http://sourceware.org/bugzilla/show_bug.cgi?id=14827

veja o comentário "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

Não vou consertar então. Talvez devêssemos adicionar algumas funções auxiliares aos pandas algum dia para fazer o corte do malloc

Entrada no FAQ, talvez?

Para registro, nós (+ @ sbneto) estamos usando isso na predução por um certo tempo, e está indo muito bem:

# 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 muito obrigado!

Mas existem outras operações afetadas :-(

É MUITO estranho que o problema acima (problema da glibc) não tenha nenhuma reação. Afeta TODOS o ambiente de PCs e servidores Linux. E nada!!!

Eu sei, você vai me dizer: ok, escreva um patch! Vou fazer (UPD: mas vai ser estranho porque não sei nada sobre o código glibc). Mas nem mesmo ninguém sabe disso.

Todos dizem: vazamentos do KDE. Quem sabe - por quê ?! Ninguém!

Código aberto? Por vergonha! Desculpe, mas é verdade para esta situação.

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

Eu acredito em você. 2 anos e nenhum movimento desse lado: /

Digo para consertar desse lado e colocar um grande comentário de culpa, porque bifurcar ali parece inviável.

@alanjds Seu código corrigiu um problema para mim que estava causando uma grande dor de cabeça. Você estaria disposto a explicar qual é o comportamento padrão do pandas e como seu código o corrige?

Você também pode contornar esse problema alternando para jemalloc como seu alocador padrão. Em vez de python script.py , execute LD_PRELOAD=/usr/lib/libjemalloc.so python script.py . Observe que o caminho para libjemalloc.so pode ser diferente em seu sistema e que primeiro você precisa instalá-lo com seu gerenciador de pacotes.

@tchristensenowlet O problema parece estar no código malloc de glibc . Aparentemente, a implementação de free não respeita um flag que deveria emitir malloc_trim após um certo limite, como você pode ver no link de @ghost . Portanto, malloc_trim nunca é chamado e há perda de memória. O que fizemos foi apenas chamar manualmente malloc_trim se a lib estiver disponível no sistema. Nós o chamamos no método __del__() , que é executado quando o objeto é coletado como lixo.

glibc.malloc.mxfast tunable foi introduzido no Glibc (https://www.gnu.org/software/libc/manual/html_node/Memory-Allocation-Tunables.html).

Acho que isso pode ser o culpado em um de nossos projetos, mas nossos usuários estão executando o Windows com Python 3.8 padrão (do site oficial) e com todas as dependências instaladas via pip. Esse problema também estaria no Windows? Em caso afirmativo, qual seria o cdll.LoadLibrary("libc.so.6") equivalente?

Edit: Eu executei os testes descritos aqui, e o lixo coletado sempre fez seu trabalho corretamente:
https://github.com/pandas-dev/pandas/issues/21353
Sistema: Windows 10
Python: 3.8.5
Pandas: 1.1.0

Esta página foi útil?
1 / 5 - 1 avaliações