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

Posted

技术标签:

【中文标题】如何在 Python 中分析内存使用情况?【英文标题】:How do I profile memory usage in Python? 【发布时间】:2009-02-16 09:34:43 【问题描述】:

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

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

【问题讨论】:

Which Python memory profiler is recommended? 的副本。恕我直言,2019 年的最佳答案是 memory_profiler 【参考方案1】:

这里已经回答了这个问题:Python memory profiler

基本上你会做类似的事情(引用自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']
>>> 

【讨论】:

官方 guppy 文档有点少;有关其他资源,请参阅 this example 和 the heapy essay。 @robguinness 降级是指被否决?这似乎不公平,因为它在某个时间点很有价值。我认为顶部的编辑说明由于 X 原因它不再有效,而是查看答案 Y 或 Z。我认为这种做法更合适。 当然,这也行得通,但不知何故,如果接受和投票最高的答案涉及一个仍然有效并得到维护的解决方案,那就太好了。 h.heap() 在我导入其他一些包后非常慢。 仅适用于 Python 2【参考方案2】:

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

当我运行那个版本时,内存使用量从 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

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

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/statmresource 模块更有用。

【讨论】:

这很好,但它似乎只在“count_prefixes()”内的函数返回时打印快照。换句话说,如果您有一些长时间运行的电话,例如long_running()count_prefixes() 函数中,在long_running() 返回之前不会打印最大RSS 值。还是我弄错了? 我认为你错了,@robguinness。 memory_monitor() 运行在与count_prefixes() 不同的线程上,因此可以影响另一个线程的唯一方法是 GIL 和我传递给 memory_monitor() 的消息队列。我怀疑当count_prefixes() 调用sleep() 时,它会鼓励线程上下文切换。如果您的long_running() 实际上并没有花费很长时间,那么线程上下文可能不会切换,直到您在count_prefixes() 中回拨sleep()。如果这没有意义,请发布一个新问题并从此处链接到它。 谢谢。我将发布一个新问题并在此处添加链接。 (我需要举例说明我遇到的问题,因为我无法共享代码的专有部分。) tracemalloc 真的很棒,但不幸的是它只考虑了 python 分配的内存,所以如果你有一些自己分配的 c/c++ 扩展,tracemalloc 不会报告它。 @stason 我认为他们必须这样做,但我不知道细节。从我提供的链接来看,听起来他们在 C 中分配内存时必须做一些特定的事情才能计算在内。【参考方案3】:

如果你只想看一个对象的内存使用情况,(answer to other question)

有一个名为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.

【讨论】:

这个asizeof和RSS有关系吗? @mousecoder:en.wikipedia.org/wiki/RSS_(disambiguation) 的哪个 RSS?网络提要?怎么样? @serv-inc Resident set size,尽管我在 Pympler 的源代码中只能找到一处提及它,而且该提及似乎与 asizeof 没有直接关系 @mousecoder asizeof 报告的内存可以贡献给 RSS,是的。我不确定您所说的“相关”是什么意思。 @serv-inc 它可能非常具体。但是对于我测量一个大型多维字典的用例,我发现tracemalloc 解决方案的速度低于一个数量级【参考方案4】:

披露:

仅适用于 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

【讨论】:

“给定函数的内存使用情况”所以你的方法没有帮助。 通过查看usage[2],您正在查看ru_maxrss,这只是驻留进程的一部分。如果进程已被交换到磁盘,即使是部分交换,这也无济于事。 resource 是一个 Unix 特定的模块,在 Windows 下不起作用。 ru_maxrss(即usage[2])的单位是kB,而不是页数,所以不需要将该数字乘以resource.getpagesize() 这对我来说什么也没打印出来。【参考方案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())
    mem_info = process.memory_info()
    return mem_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

Here is my blog 描述了所有细节。 (archived link)

【讨论】:

应该是 process.memory_info().rss 而不是 process.get_memory_info().rss,至少在 ubuntu 和 python 3.6 中是这样。相关***.com/questions/41012058/psutil-error-on-macos 您对 3.x 是正确的。我的客户使用的是 Python 2.7,而不是最新版本。 这是字节,KB,MB,什么?【参考方案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)

示例用法,假设以上代码保存为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)

【讨论】:

【参考方案7】:

使用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

【讨论】:

【参考方案8】:

也许有帮助:see additional>

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

【讨论】:

以上是关于如何在 Python 中分析内存使用情况?的主要内容,如果未能解决你的问题,请参考以下文章

如何从核心转储中分析内存使用情况?

使用 Valgrind 在 Python 程序中分析内存时遇到问题

如何在 C 中分析 openMPI 程序的内存使用情况和性能

在 PyCharm 中分析 python 时内存使用率非常高

在任务管理器和 ANTS 分析器中分析的内存使用情况

在 Linux 上的 C++ 程序中分析常驻内存使用情况和许多页面错误