Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 454 – 添加新的 tracemalloc 模块来追踪 Python 内存分配

作者:
Victor Stinner <vstinner at python.org>
BDFL 代表:
Charles-François Natali <cf.natali at gmail.com>
状态:
最终
类型:
标准跟踪
创建:
2013 年 9 月 3 日
Python 版本:
3.4
决议:
Python-Dev 消息

目录

摘要

此 PEP 提案添加一个新的 tracemalloc 模块来追踪 Python 分配的内存块。

基本原理

像 Valgrind 这样的经典通用工具可以获取分配内存块的 C 回溯。使用此类工具来分析 Python 内存分配没有帮助,因为大多数内存块是在同一个 C 函数中分配的,例如在 PyMem_Malloc() 中。此外,Python 拥有一个用于小型对象的分配器,称为“pymalloc”,它为了效率而保留空闲块。这些工具无法很好地处理这种情况。

有一些专用于 Python 语言的调试工具,例如 HeapyPymplerMeliae,它们使用垃圾回收器模块列出所有活动对象(例如 gc.get_objects()gc.get_referrers()gc.get_referents() 函数),计算其大小(例如使用 sys.getsizeof())并将对象按类型分组。这些工具提供了对应用程序内存使用情况的更好估计。当大多数内存泄漏是同一类型的实例并且此类型仅在少数几个函数中实例化时,它们非常有用。当对象类型非常常见(例如 strtuple)时,问题就出现了,并且很难确定这些对象是在哪里实例化的。

查找引用循环也是一个难题。有不同的工具可以绘制所有引用的图表。这些工具不能用于具有数千个对象的大型应用程序,因为图表太大而无法手动分析。

提案

使用来自 PEP 445 的自定义分配 API,可以轻松地在 Python 内存分配器上设置一个钩子。钩子可以检查 Python 内部以检索 Python 回溯。获取当前回溯的想法来自 faulthandler 模块。faulthandler 在崩溃时转储所有 Python 线程的回溯,这里的想法是在 Python 通过分配内存块时获取当前 Python 线程的回溯。

此 PEP 提案添加一个新的 tracemalloc 模块,这是一个调试工具,用于追踪 Python 分配的内存块。该模块提供以下信息

  • 分配对象的位置的回溯
  • 每个文件名和每个行号的已分配内存块的统计信息:已分配内存块的总大小、数量和平均大小
  • 计算两个快照之间的差异以检测内存泄漏

tracemalloc 模块的 API 类似于 faulthandler 模块的 API:enable() / start()disable() / stop()is_enabled() / is_tracing() 函数,一个环境变量(PYTHONFAULTHANDLERPYTHONTRACEMALLOC)以及一个 -X 命令行选项(-X faulthandler-X tracemalloc)。请参阅 faulthandler 模块的文档

追踪内存分配的想法并不新鲜。它最初是在 2005 年的 PySizer 项目中实现的。PySizer 的实现方式不同:回溯存储在帧对象中,并且某些 Python 类型将追踪与对象类型的名称链接起来。CPython 上的 PySizer 修补程序会增加性能和内存占用方面的开销,即使 PySizer 未被使用。tracemalloc 将回溯附加到底层(内存块),并且在模块未追踪内存分配时没有开销。

tracemalloc 模块已为 CPython 编写。其他 Python 实现可能无法提供它。

API

为了追踪 Python 分配的大多数内存块,应尽早启动该模块,方法是将 PYTHONTRACEMALLOC 环境变量设置为 1,或使用 -X tracemalloc 命令行选项。可以在运行时调用 tracemalloc.start() 函数以开始追踪 Python 内存分配。

默认情况下,已分配内存块的追踪仅存储最近的帧(1 帧)。要在启动时存储 25 帧:将 PYTHONTRACEMALLOC 环境变量设置为 25,或使用 -X tracemalloc=25 命令行选项。可以在运行时使用 set_traceback_limit() 函数设置限制。

函数

clear_traces() 函数

清除 Python 分配的内存块的追踪。

另请参阅 stop()

get_object_traceback(obj) 函数

获取 Python 对象 obj 分配位置的回溯。返回一个 Traceback 实例,如果 tracemalloc 模块未追踪内存分配或未追踪对象的分配,则返回 None

另请参阅 gc.get_referrers()sys.getsizeof() 函数。

get_traceback_limit() 函数

获取追踪中存储的回溯中的最大帧数。

tracemalloc 模块必须追踪内存分配才能获取限制,否则会引发异常。

限制由 start() 函数设置。

get_traced_memory() 函数

获取 tracemalloc 模块追踪的内存块的当前大小和最大大小,作为一个元组:(size: int, max_size: int)

get_tracemalloc_memory() 函数

获取用于存储内存块追踪的 tracemalloc 模块的内存使用情况(以字节为单位)。返回一个 int

is_tracing() 函数

如果 tracemalloc 模块正在追踪 Python 内存分配,则为 True,否则为 False

另请参阅 start()stop() 函数。

start(nframe: int=1) 函数

开始追踪 Python 内存分配:在 Python 内存分配器上安装钩子。收集到的追踪的回溯将限制为 nframe 帧。默认情况下,内存块的追踪仅存储最近的帧:限制为 1nframe 必须大于或等于 1

存储超过 1 帧仅对计算按 'traceback' 分组的统计信息或计算累积统计信息有用:请参阅 Snapshot.compare_to()Snapshot.statistics() 方法。

存储更多帧会增加 tracemalloc 模块的内存和 CPU 开销。使用 get_tracemalloc_memory() 函数来衡量 tracemalloc 模块使用了多少内存。

PYTHONTRACEMALLOC 环境变量(PYTHONTRACEMALLOC=NFRAME)和 -X tracemalloc=NFRAME 命令行选项可用于在启动时开始追踪。

另请参阅 stop()is_tracing()get_traceback_limit() 函数。

stop() 函数

停止追踪 Python 内存分配:卸载 Python 内存分配器上的钩子。还清除 Python 分配的内存块的追踪

调用 take_snapshot() 函数以在清除追踪之前获取追踪的快照。

另请参阅 start()is_tracing() 函数。

take_snapshot() 函数

获取 Python 分配的内存块的追踪的快照。返回一个新的 Snapshot 实例。

快照不包括 tracemalloc 模块开始追踪内存分配之前分配的内存块。

追踪的回溯限制为 get_traceback_limit() 帧。使用 start() 函数的 nframe 参数来存储更多帧。

tracemalloc 模块必须追踪内存分配才能获取快照,请参阅 start() 函数。

另请参阅 get_object_traceback() 函数。

过滤器

Filter(inclusive: bool, filename_pattern: str, lineno: int=None, all_frames: bool=False)

用于过滤内存块跟踪信息的过滤器。

有关filename_pattern的语法,请参阅fnmatch.fnmatch()函数。文件扩展名'.pyc''.pyo'会被替换为'.py'

示例

  • Filter(True, subprocess.__file__) 仅包含subprocess模块的跟踪信息
  • Filter(False, tracemalloc.__file__) 排除tracemalloc模块的跟踪信息
  • Filter(False, "<unknown>") 排除空回溯信息

inclusive 属性

如果inclusiveTrue(包含),则仅跟踪在名称与filename_pattern匹配的文件中第lineno行分配的内存块。

如果inclusiveFalse(排除),则忽略在名称与filename_pattern匹配的文件中第lineno行分配的内存块。

lineno 属性

过滤器的行号(int)。如果linenoNone,则过滤器匹配任何行号。

filename_pattern 属性

过滤器的文件名模式(str)。

all_frames 属性

如果all_framesTrue,则检查回溯中的所有帧。如果all_framesFalse,则仅检查最近的帧。

如果回溯限制小于2,则忽略此属性。请参阅get_traceback_limit()函数和Snapshot.traceback_limit属性。

Frame

回溯中的一个帧。

Traceback类是Frame实例的序列。

filename 属性

文件名(str)。

lineno 属性

行号(int)。

快照

Snapshot

Python分配的内存块跟踪信息的快照。

take_snapshot()函数创建一个快照实例。

compare_to(old_snapshot: Snapshot, group_by: str, cumulative: bool=False) 方法

计算与旧快照的差异。根据group_by对结果进行分组,并以排序后的StatisticDiff实例列表的形式返回统计信息。

有关group_bycumulative参数,请参阅statistics()方法。

结果根据以下顺序排序:StatisticDiff.size_diff的绝对值、StatisticDiff.sizeStatisticDiff.count_diff的绝对值、Statistic.count,最后是StatisticDiff.traceback,从大到小排序。

dump(filename) 方法

将快照写入文件。

使用load()重新加载快照。

filter_traces(filters) 方法

创建一个新的Snapshot实例,其中包含经过过滤的traces序列,filters是一个Filter实例列表。如果filters为空列表,则返回一个新的Snapshot实例,其中包含traces的副本。

所有包含过滤器同时应用,如果没有任何包含过滤器匹配则忽略跟踪。如果至少有一个排除过滤器匹配,则忽略跟踪。

load(filename) 类方法

从文件加载快照。

另请参阅dump()

statistics(group_by: str, cumulative: bool=False) 方法

根据group_by对结果进行分组,并以排序后的Statistic实例列表的形式返回统计信息。
group_by 描述
'filename' 文件名
'lineno' 文件名和行号
'traceback' 回溯

如果cumulativeTrue,则累加跟踪回溯中所有帧的内存块大小和数量,而不仅仅是最新的帧。累积模式只能与group_by等于'filename''lineno'以及traceback_limit大于1一起使用。

结果根据以下顺序排序:Statistic.sizeStatistic.count,最后是Statistic.traceback,从大到小排序。

traceback_limit 属性

存储在traces回溯中的最大帧数:在拍摄快照时get_traceback_limit()的结果。

traces 属性

Python分配的所有内存块的跟踪信息:Trace实例的序列。

序列的顺序未定义。使用Snapshot.statistics()方法获取排序后的统计信息列表。

统计信息

Statistic

内存分配的统计信息。

Snapshot.statistics()返回一个Statistic实例列表。

另请参阅StatisticDiff类。

count 属性

内存块的数量(int)。

size 属性

内存块的总大小(以字节为单位)(int)。

traceback 属性

分配内存块的回溯,Traceback实例。

统计信息差异

StatisticDiff

旧快照和新快照之间内存分配的统计差异。Snapshot 实例。

Snapshot.compare_to()返回一个StatisticDiff实例列表。另请参阅Statistic类。

count 属性

新快照中内存块的数量(int):如果内存块在新快照中已释放,则为0

count_diff 属性

旧快照和新快照之间内存块数量的差异(int):如果内存块在新快照中已分配,则为0

size 属性

新快照中内存块的总大小(以字节为单位)(int):如果内存块在新快照中已释放,则为0

size_diff 属性

旧快照和新快照之间内存块总大小的差异(以字节为单位)(int):如果内存块在新快照中已分配,则为0

traceback 属性

分配内存块的回溯,Traceback实例。

追踪

Trace

内存块的跟踪信息。

Snapshot.traces属性是Trace实例的序列。

size 属性

内存块的大小(以字节为单位)(int)。

traceback 属性

分配内存块的回溯,Traceback实例。

回溯

Traceback

从最近的帧到最旧的帧排序的Frame实例序列。

回溯至少包含1个帧。如果tracemalloc模块无法获取帧,则使用文件名"<unknown>"和行号0

拍摄快照时,跟踪的回溯将限制为get_traceback_limit()帧。请参阅take_snapshot()函数。

Trace.traceback属性是Traceback实例。

被拒绝的备选方案

记录对内存分配器的调用

另一种方法是记录对malloc()realloc()free()函数的调用。调用可以记录到文件中,或通过网络发送到另一台计算机。日志条目的示例:函数名称、内存块的大小、内存块的地址、发生分配的Python回溯、时间戳。

日志不能直接使用,获取内存的当前状态需要解析之前的日志。例如,无法直接获取Python对象的回溯信息,就像get_object_traceback(obj) 使用跟踪信息那样。

Python 使用生命周期非常短的对象,因此大量使用内存分配器。它有一个针对生命周期短的小对象(小于 512 字节)优化的分配器。例如,Python 测试套件平均每秒调用 malloc()realloc()free() 270,000 次。如果日志条目的大小为 32 字节,则日志记录每秒产生 8.2 MB 或每小时 29.0 GB。

该方案被否决,因为它效率较低且功能较少。在不同的进程或不同的计算机上解析日志比在同一进程中维护分配的内存块上的跟踪信息要慢。

先前工作

  • Python 内存验证器(2005-2013):由 Software Verification 开发的商业 Python 内存验证器。它使用 Python 反射 API。
  • PySizer:Nick Smallbone 在 2005 年 Google Summer of Code 项目中开发。
  • Heapy(2006-2013):Sverker Nilsson 编写的 Guppy-PE 项目的一部分。
  • PEP 草案:在 CPython 中支持跟踪低级内存使用情况(Brett Canon,2006)
  • Muppy:Robert Schuppenies 在 2008 年开发的项目。
  • asizeof:Jean Brouwers 编写的纯 Python 模块,用于估计对象的大小(2008)。
  • Heapmonitor:它提供对单个对象大小的估算功能,并且可以跟踪某些类的所有对象。它由 Ludwig Haehne 于 2008 年开发。
  • Pympler(2008-2011):基于 asizeof、muppy 和 HeapMonitor 的项目
  • objgraph (2008-2012)
  • Dozer:Marius Gedminas 编写的 CherryPy 内存泄漏调试器的 WSGI 中间件版本(2008-2013)
  • Meliae:John A Meinel 自 2009 年以来开发的 Python 内存使用分析器
  • gdb-heap:Dave Malcolm 编写的 Python gdb 脚本(2010-2011),用于分析堆内存的使用情况
  • memory_profiler:Fabian Pedregosa 编写的(2011-2013)
  • caulk:Ben Timby 在 2012 年编写的。

另请参见 Pympler 相关工作


来源:https://github.com/python/peps/blob/main/peps/pep-0454.rst

上次修改:2023-09-09 17:39:29 GMT