如何在Python中分析内存使用情况?

Law*_*ton 198 python memory profiling

我最近对算法感兴趣,并开始通过编写一个简单的实现,然后以各种方式优化它来探索它们.

我已经熟悉用于分析运行时的标准Python模块(对于大多数事情我已经发现IPython中的timeit魔术功能已足够),但我也对内存使用感兴趣,所以我也可以探索这些权衡(例如,缓存先前计算的值表的成本与根据需要重新计算它们的成本.是否有一个模块可以为我分析给定函数的内存使用情况?

Hub*_*ert 110

这个已经在这里得到了解答: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']
>>> 
Run Code Online (Sandbox Code Playgroud)

  • Guppy似乎不再被维护,因此我建议将此答案降级,并接受其中一个其他答案. (11认同)
  • 官方的guppy文档有点微不足道; 其他资源见[本例](http://smira.ru/wp-content/uploads/2011/08/heapy.html)和[堆积文章](http://guppy-pe.sourceforge.net/ heapy-thesis.pdf). (5认同)
  • @robguinness 降级你的意思是被否决?这似乎不公平,因为它在某个时间点很有价值。我认为顶部的编辑指出它因 X 原因不再有效,而是查看答案 Y 或 Z。我认为这个做法是比较合适的。 (2认同)

Don*_*kby 75

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)
Run Code Online (Sandbox Code Playgroud)

以下是结果:

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
Run Code Online (Sandbox Code Playgroud)

什么时候内存泄漏不是泄漏?

当计算结束时仍然保留内存时,这个例子很棒,但有时你会有分配大量内存的代码,然后释放所有内存.它在技术上不是内存泄漏,但它使用的内存比你想象的要多.如何释放内存时如何跟踪内存使用情况?如果它是您的代码,您可以添加一些调试代码以在其运行时拍摄快照.如果没有,您可以启动后台线程来监视主线程运行时的内存使用情况.

这是前面的示例,其中代码已全部移入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()
Run Code Online (Sandbox Code Playgroud)

当我运行该版本时,内存使用量从6MB减少到4KB,因为该函数在完成后释放了所有内存.

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
Run Code Online (Sandbox Code Playgroud)

现在,这是一个受另一个答案启发的版本,它启动了第二个线程来监控内存使用情况.

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()
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

如果你在Linux上,你会发现/proc/self/statmresource模块更有用.

  • `tracemalloc` 真的很棒,但不幸的是它只考虑 python 分配的内存,所以如果你有一些 c/c++ 扩展它自己的分配,`tracemalloc` 不会报告它。 (6认同)

ser*_*inc 25

如果你只想查看一个对象的内存使用情况,(回答其他问题)

有一个名为Pymplerasizeof 模块,它包含模块.

使用方法如下:

from pympler import asizeof
asizeof.asizeof(my_object)
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)
>>> 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.
Run Code Online (Sandbox Code Playgroud)

  • @mousecoder:是的,它是以字节为单位的.见修正案 (2认同)
  • @ serv-inc [常驻集大小](https://en.wikipedia.org/wiki/Resident_set_size),虽然我只能在Pympler的源代码中找到一个提及它的提及似乎并没有直接与`asizeof`相关联 (2认同)

小智 20

对于一个非常简单的方法尝试:

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 )
Run Code Online (Sandbox Code Playgroud)

只需插入using("Label")您想要查看正在发生的事情的位置即可.

  • `resource`是一个特定于Unix的模块,在Windows下不起作用. (7认同)
  • "给定函数的内存使用",因此您的方法无济于事. (6认同)
  • “ru_maxrss”(即“usage[2]”)的单位是 kB,而不是页,因此无需将该数字乘以“resource.getpagesize()”。 (2认同)

nre*_*nyi 8

一个使用 memory_profile 计算代码块/函数的内存使用情况的简单示例,同时返回函数结果:

import memory_profiler as mp

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

在运行代码之前计算内存使用情况,然后计算代码期间的最大使用情况:

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])
Run Code Online (Sandbox Code Playgroud)

计算运行函数时采样点的使用情况:

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])
Run Code Online (Sandbox Code Playgroud)

学分:@skeept


rob*_*ess 6

我认为既然已接受的答案以及投票数第二高的答案都存在一些问题,所以我想再提供一个基于Ihor B.答案的答案,并进行了一些小而重要的修改。

该解决方案允许您运行分析上或者通过包装函数调用用profile,功能和调用它通过与装饰你的函数/法@profile装饰。

当您要分析一些第三方代码而不弄乱其源代码时,第一种技术很有用,而第二种技术则比较“干净”,当您不介意修改函数/方法的源代码时,效果更好想要简介。

我还修改了输出,以便获得RSS,VMS和共享内存。我不太关心“之前”和“之后”的值,只关心增量,因此我删除了这些值(如果您要与Ihor B.的答案进行比较)。

分析代码

# 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)
Run Code Online (Sandbox Code Playgroud)

用法示例,假设上面的代码另存为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()
Run Code Online (Sandbox Code Playgroud)

这将导致输出类似于以下内容:

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
Run Code Online (Sandbox Code Playgroud)

重要的最后几点注意事项:

  1. 请记住,这种分析方法仅是近似的,因为计算机上可能会发生很多其他事情。由于垃圾收集和其他因素,增量甚至可能为零。
  2. 由于某些未知的原因,出现非常短的函数调用(例如1或2 ms),而内存使用量为零。我怀疑这是硬件/操作系统(在装有Linux的基本笔记本电脑上测试过)在内存统计信息更新频率方面的一些限制。
  3. 为了使示例简单,我没有使用任何函数参数,但是它们应该像预期的那样工作,即 profile(my_function, arg)进行概要分析my_function(arg)


Iho*_* B. 5

下面是一个简单的函数装饰器,它可以跟踪函数调用之前,函数调用之后进程消耗的内存量以及它们之间的区别:

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
Run Code Online (Sandbox Code Playgroud)

这是我的博客,描述了所有详细信息。(已归档的链接

  • 至少在ubuntu和python 3.6中,它应该是process.memory_info()。rss而不是process.get_memory_info()。rss。相关/sf/ask/2870844091/ (3认同)