Comment profiler l'utilisation de la mémoire en Python?

230

Je me suis récemment intéressé aux algorithmes et j'ai commencé à les explorer en écrivant une implémentation naïve, puis en l'optimisant de diverses manières.

Je connais déjà le module Python standard pour le profil d'exécution (pour la plupart des choses, j'ai trouvé que la fonction magique timeit dans IPython était suffisante), mais je suis également intéressé par l'utilisation de la mémoire afin que je puisse également explorer ces compromis ( par exemple, le coût de la mise en cache d'une table de valeurs précédemment calculées par rapport à leur recalcul si nécessaire). Existe-t-il un module qui profilera l'utilisation de la mémoire d'une fonction donnée pour moi?

Lawrence Johnston
la source
Dupliquer de quel profileur de mémoire Python est recommandé? . IMHO meilleure réponse en 2019 est memory_profiler
vladkha

Réponses:

118

Celui-ci a déjà été répondu ici: Profileur de mémoire Python

Fondamentalement, vous faites quelque chose comme ça (cité 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
la source
6
La documentation officielle de guppy est un peu minimale; pour d'autres ressources, voir cet exemple et l'essai lourd .
tutuDajuju
14
Guppy ne semble plus être maintenu, donc je suggère que cette réponse soit rétrogradée et l'une des autres réponses acceptée à la place.
robguinness
1
@robguinness Par rétrogradé, vous voulez dire voté? Cela ne semble pas juste, car il était valable à un moment donné. Je pense qu'une modification en haut indiquant qu'elle n'est plus valide pour la raison X et pour voir la réponse Y ou Z à la place. Je pense que cette ligne de conduite est plus appropriée.
WinEunuuchs2Unix
1
Bien sûr, cela fonctionne aussi, mais d'une manière ou d'une autre ce serait bien si la réponse acceptée et la plus votée impliquait une solution qui fonctionne toujours et qui est maintenue.
robguinness
92

Python 3.4 comprend un nouveau module: tracemalloc. Il fournit des statistiques détaillées sur le code qui alloue le plus de mémoire. Voici un exemple qui affiche les trois premières lignes allouant de la mémoire.

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)

Et voici les résultats:

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

Quand une fuite de mémoire n'est-elle pas une fuite?

Cet exemple est excellent lorsque la mémoire est toujours conservée à la fin du calcul, mais parfois vous avez du code qui alloue beaucoup de mémoire, puis libère tout cela. Ce n'est pas techniquement une fuite de mémoire, mais cela utilise plus de mémoire que vous ne le pensez. Comment pouvez-vous suivre l'utilisation de la mémoire lorsque tout est libéré? Si c'est votre code, vous pouvez probablement ajouter du code de débogage pour prendre des instantanés pendant son exécution. Sinon, vous pouvez démarrer un thread d'arrière-plan pour surveiller l'utilisation de la mémoire pendant l'exécution du thread principal.

Voici l'exemple précédent où le code a tous été déplacé dans la count_prefixes()fonction. Lorsque cette fonction revient, toute la mémoire est libérée. J'ai également ajouté quelques sleep()appels pour simuler un calcul à long terme.

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

Lorsque j'exécute cette version, l'utilisation de la mémoire est passée de 6 Mo à 4 Ko, car la fonction a libéré toute sa mémoire lorsqu'elle a 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

Voici maintenant une version inspirée d' une autre réponse qui démarre un deuxième thread pour surveiller l'utilisation de la mémoire.

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

Le resourcemodule vous permet de vérifier l'utilisation actuelle de la mémoire et d'enregistrer l'instantané de l'utilisation maximale de la mémoire. La file d'attente permet au thread principal d'indiquer au thread du moniteur de mémoire quand imprimer son rapport et l'arrêter. Lorsqu'il s'exécute, il affiche la mémoire utilisée par l' list()appel:

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 vous êtes sous Linux, vous trouverez peut-être /proc/self/statmplus utile que le resourcemodule.

Don Kirkby
la source
C'est très bien, mais il semble que les instantanés ne soient imprimés que pendant les intervalles lorsque les fonctions à l'intérieur de "count_prefixes ()" reviennent. En d'autres termes, si vous avez un appel de longue durée, par exemple long_running()à l'intérieur de la count_prefixes()fonction, les valeurs RSS maximales ne seront pas imprimées avant le long_running()retour. Ou je me trompe?
robguinness
Je pense que vous vous trompez, @robguinness. memory_monitor()s'exécute sur un thread distinct de count_prefixes(), donc les seules manières dont l'une peut affecter l'autre sont le GIL et la file d'attente de messages vers laquelle je passe memory_monitor(). Je soupçonne que lors des count_prefixes()appels sleep(), cela encourage le contexte du thread à changer. Si cela long_running()ne prend pas très longtemps, le contexte du thread peut ne pas basculer tant que vous n'avez pas sleep()rappelé l' appel count_prefixes(). Si cela n'a pas de sens, postez une nouvelle question et liez-la à partir d'ici.
Don Kirkby
Merci. Je vais poster une nouvelle question et ajouter un lien ici. (J'ai besoin de trouver un exemple du problème que je rencontre, car je ne peux pas partager les parties propriétaires du code.)
robguinness
31

Si vous voulez seulement regarder l'utilisation de la mémoire d'un objet, ( réponse à une autre question )

Il existe un module appelé Pympler qui contient le asizeof module.

Utilisez comme suit:

from pympler import asizeof
asizeof.asizeof(my_object)

Contrairement à sys.getsizeofcela, cela fonctionne pour vos objets auto-créés .

>>> 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
la source
1
Cette taille est-elle liée au RSS?
pg2455
1
@mousecoder: Quel RSS sur en.wikipedia.org/wiki/RSS_(disambiguation) ? Flux Web? Comment?
serv-inc
2
@ serv-inc Resident set size , bien que je ne puisse en trouver qu'une mention dans la source de Pympler et que cette mention ne semble pas directement liée àasizeof
jkmartindale
1
@mousecoder la mémoire signalée par asizeofpeut contribuer au RSS, oui. Je ne sais pas trop ce que vous entendez par "lié à".
OrangeDog
1
@ serv-inc son possible, il peut être très spécifique au cas. mais pour mon cas d'utilisation mesurant un grand dictionnaire multidimensionnel, j'ai trouvé une tracemallocsolution en dessous d'une amplitude plus rapide
ulkas
22

Divulgation:

  • Applicable sur Linux uniquement
  • Signale la mémoire utilisée par le processus actuel dans son ensemble, et non les fonctions individuelles à l' intérieur

Mais sympa à cause de sa simplicité:

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 )

Insérez simplement using("Label")où vous voulez voir ce qui se passe. Par exemple

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
anon
la source
6
"utilisation de la mémoire d'une fonction donnée", donc votre approche n'aide pas.
Glaslos
En regardant, usage[2]vous regardez ru_maxrss, qui n'est que la partie du processus qui est résidente . Cela n'aidera pas beaucoup si le processus a été échangé sur le disque, même partiellement.
Louis
8
resourceest un module spécifique à Unix qui ne fonctionne pas sous Windows.
Martin
1
Les unités de ru_maxrss(c'est-à-dire usage[2]) sont ko, pas des pages, il n'est donc pas nécessaire de multiplier ce nombre par resource.getpagesize().
Tey '
1
Cela ne m'a rien imprimé.
quantumpotato
7

Étant donné que la réponse acceptée et également la réponse la plus élevée suivante ont, à mon avis, quelques problèmes, je voudrais offrir une autre réponse qui est basée étroitement sur la réponse d'Ihor B. avec quelques petites mais importantes modifications.

Cette solution vous permet d'exécuter le profilage soit en encapsulant un appel de fonction avec la profilefonction et en l'appelant, soit en décorant votre fonction / méthode avec le @profiledécorateur.

La première technique est utile lorsque vous souhaitez profiler du code tiers sans toucher à sa source, tandis que la deuxième technique est un peu plus «propre» et fonctionne mieux lorsque cela ne vous dérange pas de modifier la source de la fonction / méthode que vous voulez profiler.

J'ai également modifié la sortie, afin que vous obteniez RSS, VMS et mémoire partagée. Je ne me soucie pas beaucoup des valeurs "avant" et "après", mais seulement du delta, donc je les ai supprimées (si vous comparez à la réponse d'Ihor B.).

Code de profilage

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

Exemple d'utilisation, en supposant que le code ci-dessus est enregistré sous 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()

Cela devrait entraîner une sortie similaire à celle ci-dessous:

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

Quelques notes finales importantes:

  1. Gardez à l'esprit que cette méthode de profilage ne sera qu'approximative, car beaucoup d'autres choses peuvent se produire sur la machine. En raison de la collecte des ordures et d'autres facteurs, les deltas peuvent même être nuls.
  2. Pour une raison inconnue, des appels de fonction très courts (par exemple 1 ou 2 ms) apparaissent avec une utilisation de mémoire nulle. Je soupçonne que c'est une limitation du matériel / OS (testé sur un ordinateur portable de base avec Linux) sur la fréquence de mise à jour des statistiques de mémoire.
  3. Pour garder les exemples simples, je n'ai pas utilisé d'arguments de fonction, mais ils devraient fonctionner comme on pourrait s'y attendre, c'est- profile(my_function, arg)à- dire pour profilermy_function(arg)
robguinness
la source
7

Vous trouverez ci-dessous un simple décorateur de fonction qui permet de suivre la quantité de mémoire consommée par le processus avant l'appel de fonction, après l'appel de fonction et quelle est la différence:

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

Voici mon blog qui décrit tous les détails. ( lien archivé )

Ihor B.
la source
4
ce ne devrait process.memory_info().rsspas être le cas process.get_memory_info().rss, du moins dans ubuntu et python 3.6. connexes stackoverflow.com/questions/41012058/psutil-error-on-macos
jangorecki
1
Vous avez raison pour 3.x. Mon client utilise Python 2.7, pas la dernière version.
Ihor B.15
4

peut-être que cela aide:
< voir plus >

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
la source
1

Un exemple simple pour calculer l'utilisation de la mémoire d'un bloc de codes / fonction en utilisant memory_profile, tout en retournant le résultat de la fonction:

import memory_profiler as mp

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

calculer l'utilisation de la mémoire avant d'exécuter le code, puis calculer l'utilisation maximale pendant le code:

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

calculer l'utilisation des points d'échantillonnage lors de l'exécution de la fonction:

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édits: @skeept

nremenyi
la source