Jak profilować użycie pamięci w Pythonie?

230

Ostatnio zainteresowałem się algorytmami i zacząłem je badać, pisząc naiwną implementację, a następnie optymalizując ją na różne sposoby.

Znam już standardowy moduł Pythona do profilowania środowiska wykonawczego (dla większości rzeczy uważam, że funkcja timeit magic w IPython jest wystarczająca), ale interesuje mnie również użycie pamięci, więc mogę również zbadać te kompromisy ( np. koszt buforowania tabeli wcześniej obliczonych wartości w porównaniu do ich ponownego obliczenia w razie potrzeby). Czy jest dla mnie moduł, który profiluje wykorzystanie pamięci przez daną funkcję dla mnie?

Lawrence Johnston
źródło
Duplikat Który profiler pamięci Python jest zalecany? . Najlepsza odpowiedź IMHO w 2019 r. To memory_profiler
vladkha

Odpowiedzi:

118

Na ten już odpowiedziano tutaj: profiler pamięci Python

Zasadniczo robisz coś takiego (cytowany z 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
źródło
6
Oficjalna dokumentacja gupika jest nieco minimalna; dla innych zasobów zobacz ten przykład i esej na temat leczenia .
tutuDajuju,
13
Wydaje się, że Gupik nie jest już obsługiwany, więc sugeruję obniżenie oceny tej odpowiedzi i zaakceptowanie jednej z pozostałych odpowiedzi.
robguinness
1
@robguinness Przez obniżoną ocenę masz na myśli obniżony głos? To nie wydaje się sprawiedliwe, ponieważ było cenne w pewnym momencie. Wydaje mi się, że zmiana u góry stwierdzająca, że ​​nie jest już ważna z powodu X, i zamiast tego wyświetla odpowiedź Y lub Z. Myślę, że ten sposób działania jest bardziej odpowiedni.
WinEunuuchs2Unix
1
Jasne, to też działa, ale jakoś byłoby miło, gdyby zaakceptowana i najwyżej oceniona odpowiedź dotyczyła rozwiązania, które nadal działa i jest utrzymywane.
robguinness
92

Python 3.4 zawiera nowy moduł: tracemalloc. Zapewnia szczegółowe statystyki dotyczące tego, który kod przydziela najwięcej pamięci. Oto przykład, który wyświetla trzy górne wiersze przydzielające pamięć.

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)

A oto wyniki:

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

Kiedy wyciek pamięci nie jest wyciek?

Ten przykład jest świetny, gdy pamięć jest nadal przechowywana na końcu obliczeń, ale czasami masz kod, który przydziela dużo pamięci, a następnie ją zwalnia. Technicznie nie jest to wyciek pamięci, ale zużywa więcej pamięci, niż myślisz. Jak możesz śledzić zużycie pamięci, gdy wszystko zostanie zwolnione? Jeśli to Twój kod, prawdopodobnie możesz dodać kod do debugowania, aby robić migawki podczas jego działania. Jeśli nie, możesz uruchomić wątek w tle, aby monitorować użycie pamięci podczas działania głównego wątku.

Oto poprzedni przykład, w którym kod został przeniesiony do count_prefixes()funkcji. Gdy funkcja ta powróci, cała pamięć zostanie zwolniona. Dodałem także kilka sleep()połączeń, aby zasymulować długotrwałe obliczenia.

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

Kiedy uruchamiam tę wersję, zużycie pamięci spadło z 6 MB do 4KB, ponieważ funkcja zwolniła całą swoją pamięć po zakończeniu.

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

Oto wersja inspirowana inną odpowiedzią, która rozpoczyna drugi wątek w celu monitorowania zużycia pamięci.

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

resourceModuł pozwala sprawdzić bieżące wykorzystanie pamięci i zapisać zrzut z wykorzystaniem pamięci szczyt. Kolejka pozwala wątkowi głównemu powiedzieć wątkowi monitora pamięci, kiedy wydrukować raport i zamknąć. Po uruchomieniu pokazuje pamięć używaną przez list()połączenie:

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

Jeśli korzystasz z systemu Linux, może okazać się /proc/self/statmbardziej przydatny niż resourcemoduł.

Don Kirkby
źródło
Jest to świetne, ale wydaje się, że drukuje migawki tylko w odstępach czasu, gdy zwracają się funkcje wewnątrz „count_prefixes ()”. Innymi słowy, jeśli masz jakieś długotrwałe wywołanie, np. long_running()Wewnątrz count_prefixes()funkcji, maksymalne wartości RSS nie zostaną wydrukowane, dopóki nie long_running()powróci. Czy się mylę?
robguinness
Myślę, że się mylisz, @robguinness. memory_monitor()działa w osobnym wątku count_prefixes(), więc jedynym sposobem, w jaki można wpłynąć na inny, jest GIL i kolejka komunikatów, do której przekazuję memory_monitor(). Podejrzewam, że podczas count_prefixes()połączeń sleep()zachęca kontekst wątków do zmiany. Jeśli long_running()tak naprawdę nie trwa to długo, kontekst wątku może się nie przełączyć, dopóki nie sleep()oddzwonisz count_prefixes(). Jeśli to nie ma sensu, opublikuj nowe pytanie i link do niego stąd.
Don Kirkby
Dzięki. Wyślę nowe pytanie i dodam link tutaj. (Muszę opracować przykład problemu, który mam, ponieważ nie mogę udostępniać zastrzeżonych części kodu).
robguinness
31

Jeśli chcesz tylko spojrzeć na wykorzystanie pamięci przez obiekt, ( odpowiedz na inne pytanie )

Istnieje moduł o nazwie Pympler, który zawiera asizeof moduł.

Użyj w następujący sposób:

from pympler import asizeof
asizeof.asizeof(my_object)

W przeciwieństwie do sys.getsizeoftego działa dla twoich samodzielnie stworzonych obiektów .

>>> 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
źródło
1
Czy to jest związane z RSS?
pg2455
1
@mousecoder: Który kanał RSS na en.wikipedia.org/wiki/RSS_(disambiguation) ? Kanały internetowe? W jaki sposób?
serv-inc
2
@ serv-inc Resident set size , chociaż mogę znaleźć tylko jedną wzmiankę o tym w źródle Pymplera i ta wzmianka nie wydaje się bezpośrednio związanaasizeof
jkmartindale 7'18
1
@mousecoder pamięć zgłoszona przez asizeofmoże przyczynić się do RSS, tak. Nie jestem pewien, co jeszcze rozumiesz przez „związany z”.
OrangeDog,
1
@ serv-inc jego możliwe, może być bardzo specyficzne dla konkretnego przypadku. ale dla mojego przypadku użycia jednego dużego wielowymiarowego słownika, znalazłem tracemallocrozwiązanie poniżej wielkości szybciej
ulkas
22

Ujawnienie:

  • Dotyczy tylko systemu Linux
  • Raportuje pamięć używaną przez bieżący proces jako całość, a nie poszczególne funkcje w nim zawarte

Ale miło ze względu na swoją prostotę:

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 )

Po prostu wstaw, using("Label")gdzie chcesz zobaczyć, co się dzieje. Na przykład

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
zaraz
źródło
6
„wykorzystanie pamięci przez daną funkcję”, więc twoje podejście nie pomaga.
Glaslos,
Patrząc na usage[2]siebie, patrzysz ru_maxrss, co jest tylko częścią procesu, który jest rezydentem . To niewiele pomoże, jeśli proces został zamieniony na dysk, nawet częściowo.
Louis,
8
resourceto moduł specyficzny dla Uniksa, który nie działa w systemie Windows.
Martin
1
Jednostkami ru_maxrss(to znaczy usage[2]) są KB, a nie strony, więc nie ma potrzeby pomnożenia tej liczby przez resource.getpagesize().
Tey „
1
To nic dla mnie nie wydrukowało.
quantumpotato,
7

Ponieważ zaakceptowana odpowiedź, a także kolejna najwyższa głosowana odpowiedź mają, moim zdaniem, pewne problemy, chciałbym zaoferować jeszcze jedną odpowiedź, która jest ściśle oparta na odpowiedzi Ihora B. z niewielkimi, ale istotnymi modyfikacjami.

To rozwiązanie pozwala uruchomić profilowanie albo przez zawinięcie wywołania profilefunkcji funkcją i wywołanie jej, albo przez dekorowanie funkcji / metody @profiledekoratorem.

Pierwsza technika jest przydatna, gdy chcesz profilować jakiś kod innej firmy bez bałaganu z jego źródłem, podczas gdy druga technika jest nieco „czystsza” i działa lepiej, gdy nie masz nic przeciwko modyfikowaniu źródła funkcji / metody, którą chcesz chcesz profilować.

Zmodyfikowałem również dane wyjściowe, aby uzyskać RSS, VMS i pamięć współdzieloną. Nie dbam o wartości „przed” i „po”, ale tylko o deltę, więc usunąłem je (jeśli porównujesz odpowiedź Ihora B.).

Kod profilujący

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

Przykładowe użycie, zakładając, że powyższy kod zostanie zapisany jako 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()

To powinno dać wynik podobny do poniższego:

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

Kilka ważnych uwag końcowych:

  1. Należy pamiętać, że ta metoda profilowania będzie jedynie przybliżona, ponieważ na komputerze może się dziać wiele innych rzeczy. Z powodu wyrzucania elementów bezużytecznych i innych czynników delty mogą wynosić nawet zero.
  2. Z nieznanego powodu bardzo krótkie wywołania funkcji (np. 1 lub 2 ms) pojawiają się przy zerowym zużyciu pamięci. Podejrzewam, że jest to pewne ograniczenie sprzętu / systemu operacyjnego (testowanego na podstawowym laptopie z Linuksem) co do częstotliwości aktualizacji statystyk pamięci.
  3. Dla uproszczenia przykładów nie użyłem żadnych argumentów funkcji, ale powinny one działać zgodnie z oczekiwaniami, tj. profile(my_function, arg)Profilowaćmy_function(arg)
robguinness
źródło
7

Poniżej znajduje się prosty dekorator funkcji, który pozwala śledzić, ile pamięci proces zużył przed wywołaniem funkcji, po wywołaniu funkcji i jaka jest różnica:

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

Oto mój blog, który opisuje wszystkie szczegóły. ( zarchiwizowany link )

Ihor B.
źródło
4
powinna ona być process.memory_info().rssnie process.get_memory_info().rssprzynajmniej w Ubuntu i pytona 3.6. powiązane stackoverflow.com/questions/41012058/psutil-error-on-macos
jangorecki
1
Masz rację co do wersji 3.x. Mój klient używa Python 2.7, a nie najnowszej wersji.
Ihor B.
4

może to pomoże:
< patrz dodatkowe >

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
źródło
1

Prosty przykład do obliczenia zużycia pamięci przez blok kodów / funkcji za pomocą profilu_pamięci, zwracając wynik funkcji:

import memory_profiler as mp

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

obliczyć zużycie pamięci przed uruchomieniem kodu, a następnie obliczyć maksymalne zużycie podczas kodu:

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

obliczyć zużycie w punktach próbkowania podczas działania funkcji:

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

Kredyty: @skeept

nremenyi
źródło