web-dev-qa-db-fra.com

Comment 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 différentes manières.

Je connais déjà le module standard Python pour le profilage de l'exécution (la plupart des choses que j'ai trouvées suffisantes pour la fonction magique timeit dans IPython), mais je m'intéresse également à l'utilisation de la mémoire pour pouvoir explorez également ces compromis (par exemple, le coût de la mise en cache d'un tableau 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?

187
Lawrence Johnston

On a déjà répondu à cette question ici: profileur de mémoire Python

En gros, 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']
>>> 
106
Hubert

Python 3.4 inclut 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 encore en attente à la fin du calcul, mais vous avez parfois un code qui alloue une grande quantité de mémoire et le libère ensuite. 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 quand 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ù tout le code a été déplacé dans la fonction count_prefixes(). Lorsque cette fonction revient, toute la mémoire est libérée. J'ai aussi ajouté quelques appels sleep() pour simuler un calcul long.

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

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

Maintenant, voici une version inspirée par 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 module resource 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 de moniteur de la mémoire quand imprimer son rapport et s’arrêter. Lorsqu'il est exécuté, il affiche la mémoire utilisée par l'appel 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 vous êtes sur Linux, vous pouvez trouver /proc/self/statm plus utile que le module resource.

64
Don Kirkby

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

Utilisez comme suit:

from pympler import asizeof
asizeof.asizeof(my_object)

Contrairement à sys.getsizeof, il fonctionne pour vos objets créés par vous-même .

>>> 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.
23
serv-inc

Pour une approche vraiment simple, essayez:

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 )

Il suffit d'insérer using("Label") où vous voulez voir ce qui se passe.

20
anon

Étant donné que la réponse acceptée, ainsi que la prochaine réponse la plus votée, ont, à mon avis, quelques problèmes, je voudrais offrir une autre réponse qui est étroitement basée sur la réponse de Ihor B. avec quelques modifications mineures mais importantes.

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

La première technique est utile lorsque vous souhaitez profiler du code tiers sans modifier sa source, tandis que la seconde technique est un peu "plus propre" et fonctionne mieux lorsque vous ne vous dérangez pas de modifier la source de la fonction/méthode que vous utilisez. vouloir profiler.

J'ai également modifié la sortie afin que vous obteniez RSS, VMS et la mémoire partagée. Je me fiche bien des valeurs "avant" et "après", mais seulement du delta, donc je les ai supprimées (si vous comparez à la réponse de 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.__+ ">",
                    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 le nom 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 produire 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 de nombreux autres problèmes peuvent se produire sur la machine. En raison du ramassage 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) s'affichent avec une utilisation de mémoire nulle. Je soupçonne qu'il s'agit d'une limitation du matériel/du système d'exploitation (testé sur un ordinateur portable de base avec Linux) sur la fréquence de mise à jour des statistiques de la mémoire.
  3. Pour que les exemples restent simples, je n’ai utilisé aucun argument de fonction, mais ils devraient fonctionner comme on pourrait s’y attendre, c.-à-d. profile(my_function, arg) pour profiler my_function(arg)
6
robguinness

peut-être que ça aide:
< voir supplémentaire >

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

Vous trouverez ci-dessous un simple décorateur de fonctions qui permet de suivre la quantité de mémoire utilisé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é )

3
Ihor B.