Як я можу профілювати використання пам'яті в Python?


230

Нещодавно я зацікавився алгоритмами і почав досліджувати їх, написавши наївну реалізацію, а потім оптимізував її різними способами.

Я вже знайомий зі стандартним модулем Python для профілювання часу виконання (для більшості речей я вважаю, що магічна функція timeit в IPython є достатньою), але мене також цікавить використання пам'яті, щоб я також міг вивчити ці компроміси ( наприклад, вартість кешування таблиці попередньо обчислених значень порівняно з перерахунком їх за потребою). Чи є модуль, який перегляне використання пам'яті певної функції для мене?


Відповіді:


118

На це вже відповіли тут: Профілер пам'яті Python

В основному ви робите щось подібне (цитується з 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
Офіційна документація guppy трохи мінімізована; для інших ресурсів див. цей приклад та епізод .
tutuDajuju

13
Здається, Guppy більше не підтримується, тому я пропоную зменшити цю відповідь, а замість неї прийняти одну з інших відповідей.
robguinness

1
@robguinness Під пониженням ви маєте на увазі низький голос? Це не здається справедливим, оскільки це було цінним у певний момент часу. Я думаю, що редакція вгорі стверджує, що вона більше не є дійсною з причини X, а натомість бачити відповідь Y або Z. Я думаю, що цей спосіб дій є більш доречним.
WinEunuuchs2Unix

1
Звичайно, і це працює, але якось було б добре, якби прийнята і найвища відповідь передбачала рішення, яке все ще працює і зберігається.
розбійність

92

Python 3.4 включає в себе новий модуль: tracemalloc. Він надає детальну статистику про те, який код виділяє найбільше пам'яті. Ось приклад, який відображає три перших рядки, що виділяють пам'ять.

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)

І ось результати:

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

Коли витік пам'яті не є витоком?

Цей приклад чудовий, коли пам’ять все ще зберігається в кінці обчислення, але іноді у вас є код, який виділяє багато пам’яті, а потім звільняє його всю. Це технічно не витік пам'яті, але він використовує більше пам'яті, ніж ви думаєте, що слід. Як можна відстежувати використання пам'яті, коли вона все вивільняється? Якщо це ваш код, ви, ймовірно, можете додати код налагодження, щоб робити знімки під час його запуску. Якщо ні, ви можете запустити фоновий потік для моніторингу використання пам'яті під час запуску основного потоку.

Ось попередній приклад, коли код був переміщений у count_prefixes()функцію. Після повернення цієї функції звільняється вся пам'ять. Я також додав кілька sleep()дзвінків, щоб імітувати довготривалий розрахунок.

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

Коли я запускаю цю версію, використання пам'яті перейшло з 6 Мб до 4 КБ, оскільки ця функція звільнила всю свою пам'ять після її завершення.

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

Тепер ось версія, натхненна іншою відповіддю, яка запускає другий потік для моніторингу використання пам'яті.

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

resourceМодуль дозволяє перевірити поточне використання пам'яті, і зберегти знімки за допомогою піку пам'яті. Черга дозволяє головному потоку повідомити потік монітора пам’яті, коли слід надрукувати звіт і вимкнути його. Коли він працює, він показує пам'ять, яку використовує 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

Якщо ви працюєте в Linux, ви можете знайти /proc/self/statmбільше корисного, ніж resourceмодуль.


Це чудово, але, здається, друкує знімки лише через проміжки часу, коли функції всередині "count_prefixes ()" повертаються. Іншими словами, якщо у вас є деякий тривалий дзвінок, наприклад, long_running()всередині count_prefixes()функції, максимальні значення RSS не надрукуються до long_running()повернення. Або я помиляюся?
розбійність

Я думаю, ти помилився, @robguinness. memory_monitor()працює на окремому потоці від count_prefixes(), тому єдиними способами, на які один може вплинути на інший, є GIL та черга повідомлень, які я переходжу memory_monitor(). Я підозрюю, що під час count_prefixes()дзвінків sleep()це заохочує контекст потоку до переключення. Якщо ваш файл long_running()насправді не займає дуже багато часу, контекст потоку може не перемикатися, поки ви не натиснете на sleep()дзвінок count_prefixes(). Якщо це не має сенсу, опублікуйте нове запитання та посилання на нього звідси.
Дон Кіркбі

Дякую. Я опублікую нове запитання і додам тут посилання. (Мені потрібно розробити приклад проблеми, яка виникає, оскільки я не можу поділитися власницькими частинами коду.)
robguinness

31

Якщо ви хочете лише переглянути споживання пам'яті об'єкта, ( відповідь на інше запитання )

Існує модуль під назвою Pympler, який містить asizeof модуль.

Використовуйте наступним чином:

from pympler import asizeof
asizeof.asizeof(my_object)

На відміну від цього sys.getsizeof, він працює для ваших створених об’єктів .

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

1
Чи пов'язаний цей розмір з RSS?
pg2455

1
@mousecoder: Який RSS на en.wikipedia.org/wiki/RSS_(disambiguation) ? Веб-канали? Як?
серв-інк

2
@ Resv-inc Resident set size , хоча я можу знайти лише одну згадку про нього в джерелі Pympler, і ця згадка не здається безпосередньо пов'язаноюasizeof
jkmartindale

1
@mousecoder пам'ять, про яку повідомляє, asizeofможе внести свій внесок у RSS, так. Я не впевнений, що ще ви маєте на увазі під "пов'язаним".
OrangeDog

1
@ serv-inc можливо, це може бути дуже конкретним випадком. але для моєї корисної шкали, що вимірює один великий багатовимірний словник, я знайшов tracemallocрішення нижче на масштабність швидше
ulkas

22

Розкриття інформації:

  • Застосовується лише в Linux
  • Звіти про пам'ять, що використовується поточним процесом в цілому, а не окремі функції всередині

Але приємно через свою простоту:

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 )

Просто вставте using("Label")туди, де ви хочете побачити, що відбувається. Наприклад

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
"використання пам'яті даної функції", тому ваш підхід не допомагає.
Glaslos

Дивлячись на usage[2]вас, ви дивитесь ru_maxrss, що є лише частиною процесу, який є резидентом . Це не дуже допоможе, якщо процес був замінений на диск, навіть частково.
Луї

8
resource- це специфічний модуль для Unix, який не працює під Windows.
Мартін

1
Одиниці ru_maxrss(тобто usage[2]) є кБ, а не сторінки, тому немає необхідності множувати це число на resource.getpagesize().
Тей '

1
Це для мене нічого не надрукувало.
квантова тота

7

Оскільки прийнята відповідь, а також наступна найвища відповідь, на мою думку, мають деякі проблеми, я хотів би запропонувати ще одну відповідь, яка грунтується на відповіді Ігоря Б. з деякими невеликими, але важливими змінами.

Це рішення дозволяє запускати профайлювання або за допомогою обгортання виклику функції з profileфункцією та виклику її, або шляхом декорування функції / методу за допомогою @profileдекоратора.

Перша методика корисна, коли ви хочете профілювати якийсь сторонній код, не псуючи його джерело, тоді як другий прийом трохи «чистіший» і працює краще, коли ви не заперечуєте над зміною джерела функції / методу, який ви хочу профілю.

Я також змінив вихід, щоб отримати RSS, VMS та спільну пам'ять. Мене мало хвилює значення "до" та "після", а лише дельта, тому я видалив їх (якщо ви порівнюєте з відповіддю Ігоря Б.).

Профілюючий код

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

Приклад використання, припускаючи, що наведений вище код зберігається як 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()

Це має призвести до виходу, подібного до наведеного нижче:

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

Кілька важливих заключних зауважень:

  1. Майте на увазі, що цей спосіб профілювання буде лише приблизним, оскільки на машині може статися багато іншого. Через збирання сміття та інших факторів дельти можуть бути навіть нульовими.
  2. З незрозумілої причини дуже короткі функціональні дзвінки (наприклад, 1 або 2 мс) відображаються при нульовому використанні пам'яті. Я підозрюю, що це деяке обмеження апаратного забезпечення / ОС (тестується на базовому ноутбуці з Linux) щодо частоти оновлення статистики пам'яті.
  3. Щоб зробити приклади простими, я не використовував жодних аргументів функції, але вони повинні працювати так, як можна було очікувати, тобто profile(my_function, arg)для профілюванняmy_function(arg)

7

Нижче наводиться простий декоратор функцій, який дозволяє відстежувати, скільки пам’яті споживає процес перед викликом функції, після виклику функції та яка різниця:

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

Ось мій блог, який описує всі деталі. ( заархівоване посилання )


4
вона повинна бути process.memory_info().rssНЕ process.get_memory_info().rss, принаймні , в Убунту і Python 3.6. пов'язаних stackoverflow.com/questions/41012058/psutil-error-on-macos
jangorecki

1
Ти маєш рацію на 3.x. Мій клієнт використовує Python 2.7, не найновішу версію.
Ігор Б.

4

можливо, це допоможе:
< див. додаткові >

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

1

Простий приклад для обчислення використання пам'яті блоку кодів / функції за допомогою пам'яті_профіль, повертаючи результат функції:

import memory_profiler as mp

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

обчислити використання пам'яті перед запуском коду, а потім обчислити максимальне використання протягом коду:

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

обчислити використання в точках вибірки під час роботи функції:

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

Кредити: @skeept

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.