Как мне профилировать использование памяти в Python?
Недавно я заинтересовался алгоритмами и начал изучать их, написав наивную реализацию, а затем оптимизировав ее различными способами.
Я уже знаком со стандартным модулем Python для профилирования среды выполнения (для большинства вещей я считаю, что магической функции timeit в IPython достаточно), но я также заинтересован в использовании памяти, чтобы также изучить эти компромиссы (например, стоимость кэширования таблицы ранее вычисленных значений или пересчет их при необходимости). Есть ли модуль, который будет профилировать использование памяти данной функции для меня?
9 ответов
На этот вопрос уже ответили: Профилировщик памяти 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']
>>>
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
модуль.
Для действительно простого подхода попробуйте:
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 )
Просто вставьте using("Label")
где вы хотите увидеть, что происходит.
Я только хочу посмотреть на использование памяти объектом ( ответ на другой вопрос).
Существует модуль под названием 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.
Ниже приведен простой декоратор функций, который позволяет отслеживать, сколько памяти потребляет процесс до вызова функции, после вызова функции, и в чем разница:
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
Вот мой блог, который описывает все детали.
Поскольку принятый ответ, а также следующий по количеству голосов ответ, по моему мнению, имеют некоторые проблемы, я хотел бы предложить еще один ответ, который тесно связан с ответом Игоря Б. с некоторыми небольшими, но важными изменениями.
Это решение позволяет вам запустить профилирование, заключив вызов функции в 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 мс) отображаются с нулевым использованием памяти. Я подозреваю, что это некоторое ограничение оборудования / ОС (проверено на базовом ноутбуке с Linux) на частоту обновления статистики памяти.
- Для простоты примеров я не использовал аргументы функций, но они должны работать так, как и следовало ожидать, т.е.
profile(my_function, arg)
в профильmy_function(arg)
Простой пример расчета использования памяти блоком кодов / функцией с использованием 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
Может быть, это поможет:
< см. дополнительно>
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(...)
Разные случаи использования требуют разных инструментов.
Веб-приложения страдают от утечек памяти, поэтому вам нужны инструменты, которые хорошо справляются с подобными проблемами.является прекрасным инструментом, вы можете видеть, что определенная строка кода отвечает за повышенное использование памяти.
Для обработки данных вам нужна максимальная память, потому что проблема не в утечках, а просто в выделении большого количества памяти. Представьте себе, что у вас есть одна строка кода, которая выделяет временный массив размером 10 ГБ, а затем немедленно удаляет его; Я совершал подобные ошибки.memory-profiler
никогда не уловит это, потому что использование памяти в начале и конце строки одинаково. Итак, вам нужен совсем другой тип профилировщика.
Для последнего варианта использования подходящие инструменты включают Memray и Fil, оба с открытым исходным кодом, и Sciagraph (коммерческий, но имеет бесплатный план и также выполняет профилирование ЦП).