293 votes

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

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 suis déjà familiarisé avec le module Python standard pour le profilage du temps d'exécution (pour la plupart des choses, j'ai trouvé la fonction magique timeit dans IPython suffisante), mais je suis également intéressé par l'utilisation de la mémoire afin de pouvoir 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 au recalcul de ces valeurs en cas de besoin). Existe-t-il un module capable d'établir le profil de l'utilisation de la mémoire d'une fonction donnée ?

1 votes

Duplicata de Quel profileur de mémoire Python est recommandé ? . A mon avis, la meilleure réponse en 2019 est profileur de mémoire

142voto

Hubert Points 831

Cette question a déjà été traitée ici : Profileur de mémoire Python

Fondamentalement, vous faites quelque chose comme ça (cité à partir 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']
>>>

6 votes

La documentation officielle de guppy est un peu sommaire ; pour d'autres ressources, voir cet exemple y l'essai heapy .

2 votes

@robguinness Par rétrogradé, vous voulez dire rétrogradé dans les votes ? Ce n'est pas juste, car il a été précieux à un moment donné. Je pense qu'une modification en haut de page indiquant qu'elle n'est plus valable pour une raison X et qu'il faut voir la réponse Y ou Z à la place. Je pense que cette ligne de conduite est plus appropriée.

1 votes

Bien sûr, cela fonctionne aussi, mais ce serait bien si la réponse acceptée et la plus votée impliquait une solution qui fonctionne toujours et qui est maintenue.

137voto

Don Kirkby Points 12671

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 parfait lorsque la mémoire est encore conservée à la fin du calcul, mais il arrive parfois que le code alloue beaucoup de mémoire puis la libère entièrement. Techniquement, il ne s'agit pas d'une fuite de mémoire, mais il utilise plus de mémoire que prévu. Comment pouvez-vous suivre l'utilisation de la mémoire lorsque celle-ci est entièrement libérée ? Si c'est votre code, vous pouvez probablement ajouter du code de débogage pour prendre des instantanés pendant l'exécution. Sinon, vous pouvez lancer 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ù tout le code a été déplacé dans le fichier count_prefixes() fonction. Lorsque cette fonction revient, toute la mémoire est libérée. J'ai aussi ajouté des sleep() pour simuler un calcul de longue durée.

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 à la fin de son exécution.

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 par une autre réponse qui démarre un second 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()

El resource vous permet de vérifier l'utilisation actuelle de la mémoire et de sauvegarder l'instantané de l'utilisation maximale de la mémoire. La file d'attente permet au thread principal d'indiquer au thread de surveillance de la mémoire quand imprimer son rapport et s'arrêter. Lorsqu'il s'exécute, il indique la mémoire utilisée par le thread principal. list() appeler :

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 sur Linux, vous pouvez trouver /proc/self/statm plus utile que le resource module.

38voto

user1587329 Points 91

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

37voto

anon Points 57

Divulgation :

  • Applicable sur Linux uniquement
  • La mémoire des rapports est utilisée par le processus actuel dans son ensemble, et non par les individus. fonctions sur

Mais agréable en raison 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 )

Il suffit d'insérer 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

6 votes

"utilisation de la mémoire d'une fonction donnée" donc votre approche n'est pas utile.

0 votes

En regardant usage[2] vous êtes à la recherche de ru_maxrss qui n'est que la partie du processus qui est résident . Cela n'aidera pas beaucoup si le processus a été transféré sur le disque, même partiellement.

8 votes

resource est un module spécifique à Unix qui ne fonctionne pas sous Windows.

14voto

robguinness Points 2829

Étant donné que la réponse acceptée et celle qui a reçu le plus grand nombre de votes posent, à mon avis, certains problèmes, j'aimerais proposer une autre réponse qui s'inspire étroitement de celle d'Ihor B. avec quelques modifications mineures mais importantes.

Cette solution vous permet d'exécuter le profilage sur soit en enveloppant un appel de fonction avec la balise profile et de l'appeler, o en décorant votre fonction/méthode avec la balise @profile décorateur.

La première technique est utile lorsque vous voulez profiler un code tiers sans toucher à son code source, tandis que la deuxième technique est un peu plus "propre" et fonctionne mieux lorsque vous n'avez pas peur de modifier le code source de la fonction/méthode que vous voulez profiler.

J'ai également modifié la sortie, de sorte que vous obteniez RSS, VMS et la 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és (si vous comparez avec la réponse d'Ihor B.).

Profilage du code

# 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 sauvegardé en tant que 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()

Le résultat devrait être similaire à celui présenté 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 remarques 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 déchets et d'autres facteurs, les deltas peuvent même être nuls.
  2. Pour une raison inconnue, les appels de fonction très courts (par exemple 1 ou 2 ms) apparaissent avec une utilisation de la mémoire nulle. Je soupçonne qu'il s'agit d'une limitation du matériel/système d'exploitation (testé sur un ordinateur portable de base avec Linux) sur la fréquence à laquelle les les statistiques de mémoire sont mises à jour.
  3. Pour garder les exemples simples, je n'ai pas utilisé d'arguments de fonction, mais ils devraient fonctionner comme on s'y attendrait, c'est à dire profile(my_function, arg) au profil my_function(arg)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X