Come posso profilare l'utilizzo della memoria in Python?


230

Di recente mi sono interessato agli algoritmi e ho iniziato a esplorarli scrivendo un'implementazione ingenua e quindi ottimizzandola in vari modi.

Ho già familiarità con il modulo Python standard per la profilazione del runtime (per la maggior parte delle cose ho trovato sufficiente la funzione timeit magic in IPython), ma sono anche interessato all'utilizzo della memoria in modo da poter esplorare anche quei compromessi ( ad es. il costo della memorizzazione nella cache di una tabella di valori precedentemente calcolati rispetto al loro ricalcolo secondo necessità). Esiste un modulo che profila l'utilizzo della memoria di una determinata funzione per me?


Duplicato di quale profiler di memoria Python è raccomandato? . La migliore risposta di IMHO nel 2019 è memory_profiler
vladkha il

Risposte:


118

A questo è già stata data una risposta qui: profiler di memoria Python

Fondamentalmente fai qualcosa del genere (citato da 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
La documentazione ufficiale di guppy è un po 'minimale; per altre risorse vedi questo esempio e il saggio heapy .
tutuDajuju,

14
Guppy sembra non essere più mantenuto, quindi suggerisco di ridurre questa risposta e di accettare una delle altre risposte.
rapina

1
@robguinness Per downgrade si intende down-rated? Non sembra giusto perché è stato prezioso ad un certo punto nel tempo. Penso che una modifica in alto affermando che non è più valida per la ragione X e per vedere invece la risposta Y o Z. Penso che questo modo di agire sia più appropriato.
WinEunuuchs2Unix

1
Certo, anche questo funziona, ma in qualche modo sarebbe bello se la risposta accettata e più votata riguardasse una soluzione che funziona ancora e viene mantenuta.
robguinness

92

Python 3.4 include un nuovo modulo: tracemalloc. Fornisce statistiche dettagliate su quale codice stia allocando più memoria. Ecco un esempio che mostra le prime tre righe che allocano la memoria.

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 qui ci sono i risultati:

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 una perdita di memoria non è una perdita?

Questo esempio è ottimo quando la memoria è ancora trattenuta alla fine del calcolo, ma a volte hai un codice che alloca molta memoria e quindi rilascia tutto. Tecnicamente non è una perdita di memoria, ma utilizza più memoria di quanto si pensi. Come puoi tenere traccia dell'utilizzo della memoria quando viene rilasciato tutto? Se è il tuo codice, probabilmente puoi aggiungere del codice di debug per scattare istantanee mentre è in esecuzione. In caso contrario, è possibile avviare un thread in background per monitorare l'utilizzo della memoria durante l'esecuzione del thread principale.

Ecco l'esempio precedente in cui il codice è stato spostato nella count_prefixes()funzione. Quando quella funzione ritorna, tutta la memoria viene rilasciata. Ho anche aggiunto alcune sleep()chiamate per simulare un calcolo a lungo termine.

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 eseguo quella versione, l'utilizzo della memoria è passato da 6 MB a 4KB, perché al termine la funzione ha rilasciato tutta la sua memoria.

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

Ora ecco una versione ispirata a un'altra risposta che avvia un secondo thread per monitorare l'utilizzo della memoria.

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

Il resourcemodulo consente di verificare l'utilizzo corrente della memoria e di salvare l'istantanea dall'utilizzo massimo della memoria. La coda consente al thread principale di comunicare al thread del monitor di memoria quando stampare il proprio report e arrestarlo. Quando viene eseguito, mostra la memoria utilizzata dalla list()chiamata:

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 sei su Linux, potresti trovare /proc/self/statmpiù utile del resourcemodulo.


Questo è fantastico, ma sembra stampare solo le istantanee durante gli intervalli quando ritornano le funzioni all'interno di "count_prefixes ()". In altre parole, se si dispone di una chiamata di lunga durata, ad esempio long_running()all'interno della count_prefixes()funzione, i valori RSS massimi non verranno stampati fino alla long_running()restituzione. O mi sbaglio?
rapina

Penso che ti sbagli, @robguinness. memory_monitor()è in esecuzione su un thread separato da count_prefixes(), quindi gli unici modi in cui uno può influire sull'altro sono il GIL e la coda di messaggi a cui passo memory_monitor(). Ho il sospetto che quando count_prefixes()chiama sleep(), incoraggia il contesto del thread a cambiare. Se long_running()non stai impiegando molto tempo, il contesto del thread potrebbe non cambiare fino a quando non rispondi alla sleep()chiamata count_prefixes(). Se ciò non ha senso, pubblica una nuova domanda e linka da qui.
Don Kirkby,

Grazie. Pubblicherò una nuova domanda e aggiungerò un link qui. (Ho bisogno di elaborare un esempio del problema che sto
riscontrando

31

Se vuoi solo guardare l'utilizzo della memoria di un oggetto, ( rispondi ad un'altra domanda )

C'è un modulo chiamato Pympler che contiene il asizeof modulo.

Utilizzare come segue:

from pympler import asizeof
asizeof.asizeof(my_object)

A differenza sys.getsizeof, funziona per i tuoi oggetti creati da te .

>>> 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
Questa dimensione è correlata a RSS?
pg2455,

1
@mousecoder: quale RSS su en.wikipedia.org/wiki/RSS_(disambiguation) ? Feed Web? Come?
serv-inc,

2
@ serv-inc Dimensione set residente , anche se posso trovarne solo una menzione nella fonte di Pympler e quella menzione non sembra direttamente collegata aasizeof
jkmartindale

1
@mousecoder la memoria segnalata da asizeofpuò contribuire a RSS, sì. Non sono sicuro di cos'altro intendi per "correlato a".
OrangeDog,

1
@ serv-inc è possibile che potrebbe essere molto specifico per ogni caso. ma per il mio caso che misurava un grande dizionario multidimensionale, ho trovato la tracemallocsoluzione sotto una magnitudine più veloce
ulkas

22

Divulgazione:

  • Applicabile solo su Linux
  • Riporta la memoria utilizzata dall'intero processo nel suo insieme, non le singole funzioni all'interno

Ma carino per la sua semplicità:

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 )

Inserisci semplicemente using("Label")dove vuoi vedere cosa sta succedendo. Per esempio

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
"utilizzo della memoria di una determinata funzione", quindi il tuo approccio non aiuta.
Glaslos,

Guardandoti usage[2]stai guardando ru_maxrss, che è solo la parte del processo che è residente . Questo non aiuterà molto se il processo è stato scambiato su disco, anche parzialmente.
Louis,

8
resourceè un modulo specifico di Unix che non funziona su Windows.
Martin,

1
Le unità di ru_maxrss(cioè, usage[2]) sono kB, non pagine, quindi non è necessario moltiplicare quel numero per resource.getpagesize().
Tey '

1
Questo non ha stampato nulla per me.
quantumpotato,

7

Dal momento che la risposta accettata e anche la successiva risposta più votata presentano, a mio avviso, alcuni problemi, vorrei offrire un'altra risposta che si basa strettamente sulla risposta di Ihor B. con alcune piccole ma importanti modifiche.

Questa soluzione consente di eseguire il profiling su o avvolgendo una chiamata di funzione con la profilefunzione di e chiamandolo, o per decorare la vostra funzione / metodo con l' @profiledecoratore.

La prima tecnica è utile quando vuoi profilare un codice di terze parti senza fare confusione con la sua fonte, mentre la seconda tecnica è un po 'più "pulita" e funziona meglio quando non ti dispiace modificare la fonte della funzione / metodo vuoi profilare.

Ho anche modificato l'output, in modo da ottenere RSS, VMS e memoria condivisa. Non mi interessa molto dei valori "prima" e "dopo", ma solo il delta, quindi li ho rimossi (se si sta confrontando con la risposta di Ihor B.).

Codice di profilazione

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

Esempio di utilizzo, supponendo che il codice sopra sia salvato come 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()

Ciò dovrebbe comportare un output simile al seguente:

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

Un paio di importanti note finali:

  1. Tieni presente che questo metodo di profilazione sarà solo approssimativo, dal momento che molte altre cose potrebbero accadere sulla macchina. A causa della raccolta dei rifiuti e di altri fattori, i delta potrebbero anche essere pari a zero.
  2. Per qualche ragione sconosciuta, vengono visualizzate chiamate di funzione molto brevi (ad es. 1 o 2 ms) con zero utilizzo della memoria. Sospetto che questa sia una limitazione dell'hardware / sistema operativo (testato su laptop di base con Linux) sulla frequenza con cui vengono aggiornate le statistiche sulla memoria.
  3. Per rendere semplici gli esempi, non ho usato alcun argomento di funzione, ma dovrebbero funzionare come ci si aspetterebbe, vale profile(my_function, arg)a dire il profilomy_function(arg)

7

Di seguito è riportato un semplice decoratore di funzioni che consente di tenere traccia della memoria consumata dal processo prima della chiamata di funzione, dopo la chiamata di funzione e qual è la differenza:

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

Ecco il mio blog che descrive tutti i dettagli. ( link archiviato )


4
dovrebbe essere process.memory_info().rssnon process.get_memory_info().rss, almeno in Ubuntu e python 3.6. correlati stackoverflow.com/questions/41012058/psutil-error-on-macos
jangorecki

1
Hai ragione su 3.x. Il mio cliente utilizza Python 2.7, non la versione più recente.
Ihor B.

4

forse aiuta:
< vedi ulteriori >

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

Un semplice esempio per calcolare l'utilizzo della memoria di un blocco di codici / funzione utilizzando memory_profile, restituendo il risultato della funzione:

import memory_profiler as mp

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

calcolare l'utilizzo della memoria prima di eseguire il codice, quindi calcolare l'utilizzo massimo durante il codice:

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

calcolare l'utilizzo nei punti di campionamento durante l'esecuzione della funzione:

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

Crediti: @skeept

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.