¿Cómo perfilo el uso de memoria en Python?

230

Recientemente me interesé en los algoritmos y comencé a explorarlos escribiendo una implementación ingenua y luego optimizándola de varias maneras.

Ya estoy familiarizado con el módulo estándar de Python para perfilar el tiempo de ejecución (para la mayoría de las cosas he encontrado que la función mágica timeit en IPython es suficiente), pero también estoy interesado en el uso de la memoria para poder explorar esas compensaciones también ( por ejemplo, el costo de almacenar en caché una tabla de valores calculados previamente versus volver a calcularlos según sea necesario). ¿Hay un módulo que perfile el uso de memoria de una función determinada para mí?

Lawrence Johnston
fuente
¿Duplicado de qué perfil de memoria Python se recomienda? . La mejor respuesta de la OMI
vladkha

Respuestas:

118

Esto ya ha sido respondido aquí: generador de perfiles de memoria Python

Básicamente haces algo así (citado de 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']
>>> 
Hubert
fuente
66
La documentación oficial de guppy es un poco mínima; para otros recursos vea este ejemplo y el ensayo pesado .
tutuDajuju
14
Parece que Guppy ya no se mantiene, por lo que sugiero que esta respuesta se rebaje y se acepte una de las otras respuestas.
robguinness
1
@robguinness Por degradado, ¿quiere decir que ha sido rechazado? Eso no parece justo porque fue valioso en un momento dado. Creo que una edición en la parte superior indicando que ya no es válida por razón X y para ver la respuesta Y o Z en su lugar. Creo que este curso de acción es más apropiado.
WinEunuuchs2Unix
1
Claro, eso también funciona, pero de alguna manera sería bueno si la respuesta aceptada y mejor votada involucrara una solución que aún funcione y se mantenga.
robguinness
92

Python 3.4 incluye un nuevo módulo: tracemalloc. Proporciona estadísticas detalladas sobre qué código está asignando la mayor cantidad de memoria. Aquí hay un ejemplo que muestra las tres líneas principales que asignan 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)

Y aquí están los 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

¿Cuándo es una pérdida de memoria no una pérdida?

Ese ejemplo es excelente cuando la memoria todavía se mantiene al final del cálculo, pero a veces tiene un código que asigna mucha memoria y luego lo libera todo. Técnicamente no es una pérdida de memoria, pero está usando más memoria de la que crees que debería. ¿Cómo puede rastrear el uso de la memoria cuando todo se libera? Si es su código, probablemente pueda agregar algún código de depuración para tomar instantáneas mientras se está ejecutando. De lo contrario, puede iniciar un subproceso en segundo plano para controlar el uso de la memoria mientras se ejecuta el subproceso principal.

Aquí está el ejemplo anterior donde todo el código se ha movido al count_prefixes() función. Cuando esa función regresa, se libera toda la memoria. También agregué algunas sleep()llamadas para simular un cálculo de larga duración.

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

Cuando ejecuté esa versión, el uso de la memoria pasó de 6 MB a 4 KB, porque la función liberó toda su memoria cuando terminó.

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

Ahora aquí hay una versión inspirada en otra respuesta que inicia un segundo hilo para monitorear el uso de la 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()

El resourcemódulo le permite verificar el uso actual de la memoria y guardar la instantánea del uso máximo de la memoria. La cola permite que el subproceso principal le indique al subproceso del monitor de memoria cuándo imprimir su informe y apagarlo. Cuando se ejecuta, muestra la memoria utilizada por la list()llamada:

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

Si está en Linux, puede encontrar /proc/self/statmmás útil que el resourcemódulo.

Don Kirkby
fuente
Esto es genial, pero parece que solo imprime las instantáneas durante los intervalos cuando regresan las funciones dentro de "count_prefixes ()". En otras palabras, si tiene alguna llamada de larga duración, por ejemplo, long_running()dentro de la count_prefixes()función, los valores máximos de RSS no se imprimirán hasta que long_running()regrese. ¿O me equivoco?
robguinness
Creo que te equivocas, @robguinness. memory_monitor()se ejecuta en un hilo separado de count_prefixes(), por lo que las únicas formas en que una puede afectar a la otra son GIL y la cola de mensajes a la que paso memory_monitor(). Sospecho que cuando count_prefixes()llama sleep(), alienta el contexto del hilo para cambiar. Si en long_running()realidad no está tardando mucho, entonces el contexto del hilo podría no cambiar hasta que vuelva a presionar la sleep()llamada count_prefixes(). Si eso no tiene sentido, publique una nueva pregunta y enlace desde aquí.
Don Kirkby
Gracias. Publicaré una nueva pregunta y agregaré un enlace aquí. (Necesito elaborar un ejemplo del problema que estoy teniendo, ya que no puedo compartir las partes propietarias del código.)
robguinness
31

Si solo desea ver el uso de memoria de un objeto, ( responda a otra pregunta )

Hay un módulo llamado Pympler que contiene el asizeof módulo.

Use de la siguiente manera:

from pympler import asizeof
asizeof.asizeof(my_object)

A diferencia sys.getsizeof, funciona para tus objetos creados por ti mismo .

>>> 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.
serv-inc
fuente
1
¿Está este tamaño relacionado con RSS?
pg2455
1
@mousecoder: ¿Qué RSS en en.wikipedia.org/wiki/RSS_(disambiguation) ? Feeds web? ¿Cómo?
serv-inc
2
@ serv-inc Tamaño del conjunto residente , aunque solo puedo encontrar una mención de él en la fuente de Pympler y esa mención no parece estar directamente relacionada con élasizeof
jkmartindale
1
@mousecoder la memoria informada por asizeofpuede contribuir a RSS, sí. No estoy seguro de qué más quieres decir con "relacionado con".
OrangeDog
1
@ serv-inc es posible que sea muy específico para cada caso. pero para mi caso de uso de medición multidimensional de una gran diccionario, encontré tracemallocla solución por debajo de una magnitud más rápido
ulkas
22

Divulgar:

  • Aplicable solo en Linux
  • Informes de memoria utilizada por el proceso actual en su conjunto, no funciones individuales dentro de

Pero agradable por su simplicidad:

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 )

Simplemente inserte using("Label")donde desea ver lo que está sucediendo. Por ejemplo

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
luego
fuente
66
"uso de memoria de una función determinada", por lo que su enfoque no está ayudando.
Glaslos
Al mirar, usage[2]estás mirando ru_maxrss, que es solo la parte del proceso que es residente . Esto no ayudará mucho si el proceso se ha cambiado al disco, incluso parcialmente.
Louis
8
resourcees un módulo específico de Unix que no funciona en Windows.
Martin
1
Las unidades de ru_maxrss(es decir, usage[2]) son kB, no páginas, por lo que no es necesario multiplicar ese número por resource.getpagesize().
Tey '
1
Esto no imprimió nada para mí.
quantumpotato
7

Dado que la respuesta aceptada y también la siguiente respuesta mejor votada tienen, en mi opinión, algunos problemas, me gustaría ofrecer una respuesta más que se base estrechamente en la respuesta de Ihor B. con algunas modificaciones pequeñas pero importantes.

Esta solución le permite ejecutar la creación de perfiles ya sea envolviendo una llamada de función con la profilefunción y llamándola, o decorando su función / método con@profile decorador.

La primera técnica es útil cuando desea perfilar algún código de terceros sin alterar su fuente, mientras que la segunda técnica es un poco "más limpia" y funciona mejor cuando no le importa modificar la fuente de la función / método Quiero perfilar.

También modifiqué la salida para que obtenga RSS, VMS y memoria compartida. No me importan mucho los valores "antes" y "después", sino solo el delta, así que los eliminé (si se compara con la respuesta de Ihor B.).

Código 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)

Ejemplo de uso, suponiendo que el código anterior se guarda 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()

Esto debería dar como resultado una salida similar a la siguiente:

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 par de notas finales importantes:

  1. Tenga en cuenta que este método de creación de perfiles solo será aproximado, ya que pueden estar ocurriendo muchas otras cosas en la máquina. Debido a la recolección de basura y otros factores, los deltas podrían incluso ser cero.
  2. Por alguna razón desconocida, las llamadas de función muy cortas (por ejemplo, 1 o 2 ms) aparecen con un uso de memoria cero. Sospecho que esta es una limitación del hardware / sistema operativo (probado en una computadora portátil básica con Linux) sobre la frecuencia con la que se actualizan las estadísticas de memoria.
  3. Para mantener los ejemplos simples, no utilicé ningún argumento de función, pero deberían funcionar como cabría esperar, es decir, profile(my_function, arg)para perfilarmy_function(arg)
robguinness
fuente
7

A continuación se muestra un decorador de funciones simple que permite rastrear cuánta memoria consumió el proceso antes de la llamada a la función, después de la llamada a la función, y cuál es la diferencia:

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

Aquí está mi blog que describe todos los detalles. ( enlace archivado )

Ihor B.
fuente
44
no debería ser process.memory_info().rssasí process.get_memory_info().rss, al menos en ubuntu y python 3.6. relacionados stackoverflow.com/questions/41012058/psutil-error-on-macos
jangorecki
1
Tienes razón en cuanto a 3.x. Mi cliente está usando Python 2.7, no la versión más nueva.
Ihor B.
4

tal vez ayude:
< ver más >

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(...)
madjardi
fuente
1

Un ejemplo simple para calcular el uso de memoria de un bloque de códigos / función usando memory_profile, mientras se devuelve el resultado de la función:

import memory_profiler as mp

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

calcular el uso de memoria antes de ejecutar el código y luego calcular el uso máximo durante el 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 el uso en puntos de muestreo mientras se ejecuta la función:

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

nremenyi
fuente