Como perfil o uso de memória no Python?


230

Recentemente, me interessei por algoritmos e comecei a explorá-los escrevendo uma implementação ingênua e otimizando-a de várias maneiras.

Eu já estou familiarizado com o módulo Python padrão para criação de perfil em tempo de execução (para a maioria das coisas, achei a função mágica timeit no IPython suficiente), mas também estou interessado no uso de memória para poder explorar essas vantagens também ( por exemplo, o custo de armazenar em cache uma tabela de valores calculados anteriormente versus recalculá-los conforme necessário). Existe um módulo que analise o uso de memória de uma determinada função para mim?


Duplicar de Que criador de perfil de memória Python é recomendado? . A melhor resposta do IMHO em 2019 é memory_profiler
vladkha

Respostas:


118

Este já foi respondido aqui: Python memory profiler

Basicamente, você faz algo assim (citado em Guppy-PE ):

>>> from guppy import hpy; h=hpy()
>>> h.heap()
Partition of a set of 48477 objects. Total size = 3265516 bytes.
 Index  Count   %     Size   % Cumulative  % Kind (class / dict of class)
     0  25773  53  1612820  49   1612820  49 str
     1  11699  24   483960  15   2096780  64 tuple
     2    174   0   241584   7   2338364  72 dict of module
     3   3478   7   222592   7   2560956  78 types.CodeType
     4   3296   7   184576   6   2745532  84 function
     5    401   1   175112   5   2920644  89 dict of class
     6    108   0    81888   3   3002532  92 dict (no owner)
     7    114   0    79632   2   3082164  94 dict of type
     8    117   0    51336   2   3133500  96 type
     9    667   1    24012   1   3157512  97 __builtin__.wrapper_descriptor
<76 more rows. Type e.g. '_.more' to view.>
>>> h.iso(1,[],{})
Partition of a set of 3 objects. Total size = 176 bytes.
 Index  Count   %     Size   % Cumulative  % Kind (class / dict of class)
     0      1  33      136  77       136  77 dict (no owner)
     1      1  33       28  16       164  93 list
     2      1  33       12   7       176 100 int
>>> x=[]
>>> h.iso(x).sp
 0: h.Root.i0_modules['__main__'].__dict__['x']
>>> 

6
A documentação oficial do guppy é um pouco minimizada; para outros recursos, veja este exemplo e o ensaio pesado .
TutuDajuju 01/07/2015

14
O Guppy parece não ser mais mantido, então sugiro que essa resposta seja rebaixada e que uma das outras respostas seja aceita.
robguinness

1
@robguinness Por rebaixado, você quer dizer votado para baixo? Isso não parece justo porque foi valioso em um determinado momento. Eu acho que uma edição na parte superior afirmando que não é mais válida pelo motivo X e, em vez disso, veja a resposta Y ou Z. Eu acho que esse curso de ação é mais apropriado.
WinEunuuchs2Unix 31/12/19

1
Claro, isso também funciona, mas, de alguma forma, seria bom se a resposta aceita e com o voto mais alto envolvesse uma solução que ainda funcione e seja mantida.
robguinness

92

Python 3.4 inclui um novo módulo: tracemalloc. Ele fornece estatísticas detalhadas sobre qual código está alocando mais memória. Aqui está um exemplo que exibe as três principais linhas que alocam memória.

from collections import Counter
import linecache
import os
import tracemalloc

def display_top(snapshot, key_type='lineno', limit=3):
    snapshot = snapshot.filter_traces((
        tracemalloc.Filter(False, "<frozen importlib._bootstrap>"),
        tracemalloc.Filter(False, "<unknown>"),
    ))
    top_stats = snapshot.statistics(key_type)

    print("Top %s lines" % limit)
    for index, stat in enumerate(top_stats[:limit], 1):
        frame = stat.traceback[0]
        # replace "/path/to/module/file.py" with "module/file.py"
        filename = os.sep.join(frame.filename.split(os.sep)[-2:])
        print("#%s: %s:%s: %.1f KiB"
              % (index, filename, frame.lineno, stat.size / 1024))
        line = linecache.getline(frame.filename, frame.lineno).strip()
        if line:
            print('    %s' % line)

    other = top_stats[limit:]
    if other:
        size = sum(stat.size for stat in other)
        print("%s other: %.1f KiB" % (len(other), size / 1024))
    total = sum(stat.size for stat in top_stats)
    print("Total allocated size: %.1f KiB" % (total / 1024))


tracemalloc.start()

counts = Counter()
fname = '/usr/share/dict/american-english'
with open(fname) as words:
    words = list(words)
    for word in words:
        prefix = word[:3]
        counts[prefix] += 1
print('Top prefixes:', counts.most_common(3))

snapshot = tracemalloc.take_snapshot()
display_top(snapshot)

E aqui estão os resultados:

Top prefixes: [('con', 1220), ('dis', 1002), ('pro', 809)]
Top 3 lines
#1: scratches/memory_test.py:37: 6527.1 KiB
    words = list(words)
#2: scratches/memory_test.py:39: 247.7 KiB
    prefix = word[:3]
#3: scratches/memory_test.py:40: 193.0 KiB
    counts[prefix] += 1
4 other: 4.3 KiB
Total allocated size: 6972.1 KiB

Quando um vazamento de memória não é um vazamento?

Esse exemplo é ótimo quando a memória ainda está sendo mantida no final do cálculo, mas às vezes você tem um código que aloca muita memória e libera tudo. Tecnicamente, não é um vazamento de memória, mas está usando mais memória do que você imagina. Como você pode rastrear o uso da memória quando tudo é lançado? Se for o seu código, você provavelmente poderá adicionar algum código de depuração para tirar instantâneos enquanto estiver em execução. Caso contrário, você pode iniciar um thread em segundo plano para monitorar o uso da memória enquanto o thread principal é executado.

Aqui está o exemplo anterior, onde todo o código foi movido para a count_prefixes()função Quando essa função retorna, toda a memória é liberada. Também adicionei algumas sleep()chamadas para simular um cálculo de longa duração.

from collections import Counter
import linecache
import os
import tracemalloc
from time import sleep


def count_prefixes():
    sleep(2)  # Start up time.
    counts = Counter()
    fname = '/usr/share/dict/american-english'
    with open(fname) as words:
        words = list(words)
        for word in words:
            prefix = word[:3]
            counts[prefix] += 1
            sleep(0.0001)
    most_common = counts.most_common(3)
    sleep(3)  # Shut down time.
    return most_common


def main():
    tracemalloc.start()

    most_common = count_prefixes()
    print('Top prefixes:', most_common)

    snapshot = tracemalloc.take_snapshot()
    display_top(snapshot)


def display_top(snapshot, key_type='lineno', limit=3):
    snapshot = snapshot.filter_traces((
        tracemalloc.Filter(False, "<frozen importlib._bootstrap>"),
        tracemalloc.Filter(False, "<unknown>"),
    ))
    top_stats = snapshot.statistics(key_type)

    print("Top %s lines" % limit)
    for index, stat in enumerate(top_stats[:limit], 1):
        frame = stat.traceback[0]
        # replace "/path/to/module/file.py" with "module/file.py"
        filename = os.sep.join(frame.filename.split(os.sep)[-2:])
        print("#%s: %s:%s: %.1f KiB"
              % (index, filename, frame.lineno, stat.size / 1024))
        line = linecache.getline(frame.filename, frame.lineno).strip()
        if line:
            print('    %s' % line)

    other = top_stats[limit:]
    if other:
        size = sum(stat.size for stat in other)
        print("%s other: %.1f KiB" % (len(other), size / 1024))
    total = sum(stat.size for stat in top_stats)
    print("Total allocated size: %.1f KiB" % (total / 1024))


main()

Quando executo essa versão, o uso de memória diminuiu de 6 MB para 4KB, porque a função liberou toda a memória ao terminar.

Top prefixes: [('con', 1220), ('dis', 1002), ('pro', 809)]
Top 3 lines
#1: collections/__init__.py:537: 0.7 KiB
    self.update(*args, **kwds)
#2: collections/__init__.py:555: 0.6 KiB
    return _heapq.nlargest(n, self.items(), key=_itemgetter(1))
#3: python3.6/heapq.py:569: 0.5 KiB
    result = [(key(elem), i, elem) for i, elem in zip(range(0, -n, -1), it)]
10 other: 2.2 KiB
Total allocated size: 4.0 KiB

Agora, aqui está uma versão inspirada em outra resposta que inicia um segundo thread para monitorar o uso da memória.

from collections import Counter
import linecache
import os
import tracemalloc
from datetime import datetime
from queue import Queue, Empty
from resource import getrusage, RUSAGE_SELF
from threading import Thread
from time import sleep

def memory_monitor(command_queue: Queue, poll_interval=1):
    tracemalloc.start()
    old_max = 0
    snapshot = None
    while True:
        try:
            command_queue.get(timeout=poll_interval)
            if snapshot is not None:
                print(datetime.now())
                display_top(snapshot)

            return
        except Empty:
            max_rss = getrusage(RUSAGE_SELF).ru_maxrss
            if max_rss > old_max:
                old_max = max_rss
                snapshot = tracemalloc.take_snapshot()
                print(datetime.now(), 'max RSS', max_rss)


def count_prefixes():
    sleep(2)  # Start up time.
    counts = Counter()
    fname = '/usr/share/dict/american-english'
    with open(fname) as words:
        words = list(words)
        for word in words:
            prefix = word[:3]
            counts[prefix] += 1
            sleep(0.0001)
    most_common = counts.most_common(3)
    sleep(3)  # Shut down time.
    return most_common


def main():
    queue = Queue()
    poll_interval = 0.1
    monitor_thread = Thread(target=memory_monitor, args=(queue, poll_interval))
    monitor_thread.start()
    try:
        most_common = count_prefixes()
        print('Top prefixes:', most_common)
    finally:
        queue.put('stop')
        monitor_thread.join()


def display_top(snapshot, key_type='lineno', limit=3):
    snapshot = snapshot.filter_traces((
        tracemalloc.Filter(False, "<frozen importlib._bootstrap>"),
        tracemalloc.Filter(False, "<unknown>"),
    ))
    top_stats = snapshot.statistics(key_type)

    print("Top %s lines" % limit)
    for index, stat in enumerate(top_stats[:limit], 1):
        frame = stat.traceback[0]
        # replace "/path/to/module/file.py" with "module/file.py"
        filename = os.sep.join(frame.filename.split(os.sep)[-2:])
        print("#%s: %s:%s: %.1f KiB"
              % (index, filename, frame.lineno, stat.size / 1024))
        line = linecache.getline(frame.filename, frame.lineno).strip()
        if line:
            print('    %s' % line)

    other = top_stats[limit:]
    if other:
        size = sum(stat.size for stat in other)
        print("%s other: %.1f KiB" % (len(other), size / 1024))
    total = sum(stat.size for stat in top_stats)
    print("Total allocated size: %.1f KiB" % (total / 1024))


main()

O resourcemódulo permite verificar o uso atual da memória e salvar o instantâneo do pico de uso da memória. A fila permite que o thread principal informe ao thread do monitor de memória quando imprimir seu relatório e desligar. Quando executado, mostra a memória que está sendo usada pela list()chamada:

2018-05-29 10:34:34.441334 max RSS 10188
2018-05-29 10:34:36.475707 max RSS 23588
2018-05-29 10:34:36.616524 max RSS 38104
2018-05-29 10:34:36.772978 max RSS 45924
2018-05-29 10:34:36.929688 max RSS 46824
2018-05-29 10:34:37.087554 max RSS 46852
Top prefixes: [('con', 1220), ('dis', 1002), ('pro', 809)]
2018-05-29 10:34:56.281262
Top 3 lines
#1: scratches/scratch.py:36: 6527.0 KiB
    words = list(words)
#2: scratches/scratch.py:38: 16.4 KiB
    prefix = word[:3]
#3: scratches/scratch.py:39: 10.1 KiB
    counts[prefix] += 1
19 other: 10.8 KiB
Total allocated size: 6564.3 KiB

Se você estiver no Linux, poderá achar /proc/self/statmmais útil que o resourcemódulo.


Isso é ótimo, mas parece imprimir apenas os instantâneos durante os intervalos quando as funções dentro de "count_prefixes ()" retornam. Em outras palavras, se você tiver alguma chamada de longa duração, por exemplo, long_running()dentro da count_prefixes()função, os valores máximos de RSS não serão impressos até que long_running()retorne. Ou estou enganado?
robguinness

Acho que você está enganado, @robguinness. memory_monitor()está sendo executado em um encadeamento separado count_prefixes(), portanto, as únicas maneiras pelas quais um pode afetar o outro são o GIL e a fila de mensagens para as quais passo memory_monitor(). Eu suspeito que, quando count_prefixes()chamadas sleep(), isso incentiva o contexto do thread a alternar. Se você long_running()não estiver demorando muito, o contexto do encadeamento poderá não ser alterado até você sleep()retornar a chamada count_prefixes(). Se isso não fizer sentido, poste uma nova pergunta e vincule-a aqui.
Don Kirkby

Obrigado. Vou postar uma nova pergunta e adicionar um link aqui. (Eu preciso trabalhar-se um exemplo do problema que estou tendo, já que não posso compartilhar as peças proprietárias do código.)
robguinness

31

Se você deseja apenas examinar o uso de memória de um objeto, ( responda a outra pergunta )

Existe um módulo chamado Pympler que contém oasizeof módulo.

Use da seguinte maneira:

from pympler import asizeof
asizeof.asizeof(my_object)

Ao contrário sys.getsizeof, ele funciona para seus objetos criados automaticamente .

>>> asizeof.asizeof(tuple('bcd'))
200
>>> asizeof.asizeof({'foo': 'bar', 'baz': 'bar'})
400
>>> asizeof.asizeof({})
280
>>> asizeof.asizeof({'foo':'bar'})
360
>>> asizeof.asizeof('foo')
40
>>> asizeof.asizeof(Bar())
352
>>> asizeof.asizeof(Bar().__dict__)
280
>>> help(asizeof.asizeof)
Help on function asizeof in module pympler.asizeof:

asizeof(*objs, **opts)
    Return the combined size in bytes of all objects passed as positional arguments.

1
Isso está relacionado ao RSS?
Pg2455

1
@mousecoder: qual RSS em en.wikipedia.org/wiki/RSS_(disambiguation) ? Feeds da Web? Quão?
serv-inc

2
@ serv-inc tamanho do conjunto residente , embora eu só posso encontrar uma menção a ele na fonte de Pympler e que a menção não parece diretamente ligadaasizeof
jkmartindale

1
@mousecoder a memória relatada por asizeofpode contribuir para o RSS, sim. Não sei ao certo o que você quer dizer com "relacionado a".
OrangeDog

1
@ serv-inc é possível, pode ser muito específico para cada caso. mas para o meu medição usecase um grande multidimensional dicionário, eu encontrei tracemallocsolução abaixo de uma magnitude mais rápido
ulkas

22

Divulgação:

  • Aplicável apenas no Linux
  • Informa a memória usada pelo processo atual como um todo, e não as funções individuais dentro

Mas legal por causa de sua simplicidade:

import resource
def using(point=""):
    usage=resource.getrusage(resource.RUSAGE_SELF)
    return '''%s: usertime=%s systime=%s mem=%s mb
           '''%(point,usage[0],usage[1],
                usage[2]/1024.0 )

Basta inserir using("Label")onde deseja ver o que está acontecendo. Por exemplo

print(using("before"))
wrk = ["wasting mem"] * 1000000
print(using("after"))

>>> before: usertime=2.117053 systime=1.703466 mem=53.97265625 mb
>>> after: usertime=2.12023 systime=1.70708 mem=60.8828125 mb

6
"uso de memória de uma determinada função" para que sua abordagem não esteja ajudando.
Glaslos

Ao olhar para o que usage[2]você está olhando ru_maxrss, é apenas a parte do processo que é residente . Isso não ajudará muito se o processo tiver sido trocado para o disco, mesmo que parcialmente.
Louis

8
resourceé um módulo específico do Unix que não funciona no Windows.
Martin

1
As unidades de ru_maxrss(ou seja, usage[2]) são kB, não páginas, portanto não há necessidade de multiplicar esse número por resource.getpagesize().
Tey '

1
Isso não imprimiu nada para mim.
Quantumpotato

7

Como a resposta aceita e também a próxima resposta mais votada têm, na minha opinião, alguns problemas, gostaria de oferecer mais uma resposta que se baseia estreitamente na resposta de Ihor B. com algumas pequenas mas importantes modificações.

Essa solução permite executar a criação de perfil , envolvendo uma chamada de função com a profilefunção e chamando-a ou decorando sua função / método com o@profile decorador.

A primeira técnica é útil quando você deseja criar um perfil de código de terceiros sem mexer com sua fonte, enquanto a segunda técnica é um pouco "mais limpa" e funciona melhor quando você não se importa de modificar a fonte da função / método que você deseja criar um perfil.

Também modifiquei a saída para obter RSS, VMS e memória compartilhada. Eu não me importo muito com os valores "antes" e "depois", mas apenas com o delta, então os removi (se você estiver comparando com a resposta de Ihor B.).

Código de criação de perfil

# profile.py
import time
import os
import psutil
import inspect


def elapsed_since(start):
    #return time.strftime("%H:%M:%S", time.gmtime(time.time() - start))
    elapsed = time.time() - start
    if elapsed < 1:
        return str(round(elapsed*1000,2)) + "ms"
    if elapsed < 60:
        return str(round(elapsed, 2)) + "s"
    if elapsed < 3600:
        return str(round(elapsed/60, 2)) + "min"
    else:
        return str(round(elapsed / 3600, 2)) + "hrs"


def get_process_memory():
    process = psutil.Process(os.getpid())
    mi = process.memory_info()
    return mi.rss, mi.vms, mi.shared


def format_bytes(bytes):
    if abs(bytes) < 1000:
        return str(bytes)+"B"
    elif abs(bytes) < 1e6:
        return str(round(bytes/1e3,2)) + "kB"
    elif abs(bytes) < 1e9:
        return str(round(bytes / 1e6, 2)) + "MB"
    else:
        return str(round(bytes / 1e9, 2)) + "GB"


def profile(func, *args, **kwargs):
    def wrapper(*args, **kwargs):
        rss_before, vms_before, shared_before = get_process_memory()
        start = time.time()
        result = func(*args, **kwargs)
        elapsed_time = elapsed_since(start)
        rss_after, vms_after, shared_after = get_process_memory()
        print("Profiling: {:>20}  RSS: {:>8} | VMS: {:>8} | SHR {"
              ":>8} | time: {:>8}"
            .format("<" + func.__name__ + ">",
                    format_bytes(rss_after - rss_before),
                    format_bytes(vms_after - vms_before),
                    format_bytes(shared_after - shared_before),
                    elapsed_time))
        return result
    if inspect.isfunction(func):
        return wrapper
    elif inspect.ismethod(func):
        return wrapper(*args,**kwargs)

Exemplo de uso, supondo que o código acima seja salvo como profile.py:

from profile import profile
from time import sleep
from sklearn import datasets # Just an example of 3rd party function call


# Method 1
run_profiling = profile(datasets.load_digits)
data = run_profiling()

# Method 2
@profile
def my_function():
    # do some stuff
    a_list = []
    for i in range(1,100000):
        a_list.append(i)
    return a_list


res = my_function()

Isso deve resultar em uma saída semelhante à abaixo:

Profiling:        <load_digits>  RSS:   5.07MB | VMS:   4.91MB | SHR  73.73kB | time:  89.99ms
Profiling:        <my_function>  RSS:   1.06MB | VMS:   1.35MB | SHR       0B | time:   8.43ms

Algumas notas finais importantes:

  1. Lembre-se de que esse método de criação de perfil só será aproximado, pois muitas outras coisas podem estar acontecendo na máquina. Devido à coleta de lixo e outros fatores, os deltas podem até ser zero.
  2. Por algum motivo desconhecido, chamadas de função muito curtas (por exemplo, 1 ou 2 ms) são exibidas com zero uso de memória. Eu suspeito que isso seja uma limitação do hardware / sistema operacional (testado no laptop básico com Linux) sobre a frequência com que as estatísticas de memória são atualizadas.
  3. Para manter os exemplos simples, não usei nenhum argumento de função, mas eles devem funcionar como seria de esperar, ou seja, profile(my_function, arg)criar um perfilmy_function(arg)

7

Abaixo está um simples decorador de funções que permite rastrear quanta memória o processo consumiu antes da chamada da função, após a chamada da função e qual é a diferença:

import time
import os
import psutil


def elapsed_since(start):
    return time.strftime("%H:%M:%S", time.gmtime(time.time() - start))


def get_process_memory():
    process = psutil.Process(os.getpid())
    return process.get_memory_info().rss


def profile(func):
    def wrapper(*args, **kwargs):
        mem_before = get_process_memory()
        start = time.time()
        result = func(*args, **kwargs)
        elapsed_time = elapsed_since(start)
        mem_after = get_process_memory()
        print("{}: memory before: {:,}, after: {:,}, consumed: {:,}; exec time: {}".format(
            func.__name__,
            mem_before, mem_after, mem_after - mem_before,
            elapsed_time))
        return result
    return wrapper

Aqui está o meu blog que descreve todos os detalhes. ( link arquivado )


4
ele deve ser process.memory_info().rssnão process.get_memory_info().rss, pelo menos em ubuntu e pitão 3,6. stackoverflow.com/questions/41012058/psutil-error-on-macos
jangorecki

1
Você está certo quanto ao 3.x. Meu cliente está usando o Python 2.7, não a versão mais recente.
precisa

4

talvez ajude:
< veja mais >

pip install gprof2dot
sudo apt-get install graphviz

gprof2dot -f pstats profile_for_func1_001 | dot -Tpng -o profile.png

def profileit(name):
    """
    @profileit("profile_for_func1_001")
    """
    def inner(func):
        def wrapper(*args, **kwargs):
            prof = cProfile.Profile()
            retval = prof.runcall(func, *args, **kwargs)
            # Note use of name from outer scope
            prof.dump_stats(name)
            return retval
        return wrapper
    return inner

@profileit("profile_for_func1_001")
def func1(...)

1

Um exemplo simples para calcular o uso de memória de um bloco de códigos / função usando memory_profile, enquanto retorna o resultado da função:

import memory_profiler as mp

def fun(n):
    tmp = []
    for i in range(n):
        tmp.extend(list(range(i*i)))
    return "XXXXX"

calcule o uso da memória antes de executar o código e calcule o uso máximo durante o código:

start_mem = mp.memory_usage(max_usage=True)
res = mp.memory_usage(proc=(fun, [100]), max_usage=True, retval=True) 
print('start mem', start_mem)
print('max mem', res[0][0])
print('used mem', res[0][0]-start_mem)
print('fun output', res[1])

calcular o uso em pontos de amostragem durante a execução da função:

res = mp.memory_usage((fun, [100]), interval=.001, retval=True)
print('min mem', min(res[0]))
print('max mem', max(res[0]))
print('used mem', max(res[0])-min(res[0]))
print('fun output', res[1])

Créditos: @skeept

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.