Как мне профилировать использование памяти в 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
Официальная документация гуппи немного минимальна; для других ресурсов см. этот пример и сочинение .
tutuDajuju

14
Гуппи, похоже, больше не поддерживается, поэтому я предлагаю понизить этот ответ и вместо него принять один из других ответов.
Робиннес

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
Это asizeof связано с RSS?
pg2455

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

2
@ serv-inc Резидентный размер набора , хотя я могу найти только одно упоминание об этом в источнике Пимплера, и это упоминание, похоже, не связано напрямую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().
Tey '

1
Это ничего не распечатало для меня.
Quantumpotato

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

Простой пример для вычисления использования памяти блоком кодов / функции с использованием memory_profile с возвратом результата функции:

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.