¿Cómo puedo hacer un perfil del uso de memoria en Python?

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 la creación de perfiles en tiempo de ejecución (para la mayoría de las cosas, he encontrado que la función magic time 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 previamente calculados en lugar de volver a calcularlos según sea necesario). ¿Hay algún módulo que describa el uso de memoria de una función determinada para mí?

Este ya ha sido respondido aquí: Python memory profiler

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 eg '_.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'] >>> 

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 superiores asignando 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, ""), tracemalloc.Filter(False, ""), )) 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 aún se mantiene al final del cálculo, pero a veces tiene un código que asigna mucha memoria y luego la libera. Técnicamente no es una pérdida de memoria, pero está usando más memoria de la que crees que debería. ¿Cómo se puede hacer un seguimiento del uso de la memoria cuando todo se libera? Si es su código, probablemente pueda agregar un código de depuración para tomar instantáneas mientras se está ejecutando. De lo contrario, puede iniciar un hilo de fondo para monitorear el uso de la memoria mientras se ejecuta el hilo principal.

Aquí está el ejemplo anterior donde el código se ha movido a la función count_prefixes() . Cuando esa función vuelve, toda la memoria se libera. También agregué algunas llamadas de sleep() para simular un cálculo de larga ejecució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, ""), tracemalloc.Filter(False, ""), )) 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 ejecuto esa versión, el uso de la memoria ha pasado de 6MB a 4KB, 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 comienza 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, ""), tracemalloc.Filter(False, ""), )) 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 módulo de resource permite verificar el uso de memoria actual y guardar la instantánea del uso máximo de memoria. La cola permite que el hilo principal le indique al hilo del monitor de memoria cuándo debe imprimir su informe y apagarlo. Cuando se ejecuta, muestra la memoria utilizada por la llamada de list() :

 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 que /proc/self/statm más útil que el módulo de resource .

Para un enfoque realmente simple intente:

 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]*resource.getpagesize())/1000000.0 ) 

Simplemente inserte using("Label") donde quiera ver lo que está pasando.

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

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

Use de la siguiente manera:

 from pympler import asizeof asizeof.asizeof(my_object) 

A diferencia de sys.getsizeof , funciona para tus objetos de creación propia .

 >>> 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. 

tal vez ayude
< ver adicional >

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

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

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

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 “limpia” y funciona mejor cuando no le importa modificar la fuente de la función / método que utiliza. quiero perfil

También he modificado la salida, para que obtenga RSS, VMS y memoria compartida. No me importan mucho los valores de “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, asumiendo 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 lugar a una salida similar a la siguiente:

 Profiling:  RSS: 5.07MB | VMS: 4.91MB | SHR 73.73kB | time: 89.99ms Profiling:  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 perfilado solo será aproximado, ya que muchas otras cosas podrían estar ocurriendo en la máquina. Debido a la recolección de basura y otros factores, los deltas pueden 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 esto es una limitación del hardware / sistema operativo (probado en computadoras portátiles básicas con Linux) en la frecuencia con la que se actualizan las estadísticas de memoria.
  3. Para mantener los ejemplos simples, no profile(my_function, arg) ningún argumento de función, pero deberían funcionar como cabría esperar, es decir, profile(my_function, arg) para perfilar my_function(arg)

A continuación se muestra un simple decorador de funciones que permite rastrear cuánta memoria consumió el proceso antes de la llamada de la función, después de la llamada de 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 )