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 语言的调试工具,例如 Heapy
、Pympler
和 Meliae
,它们使用垃圾回收器模块列出所有活动对象(例如 gc.get_objects()
、gc.get_referrers()
和 gc.get_referents()
函数),计算其大小(例如使用 sys.getsizeof()
)并将对象按类型分组。这些工具提供了对应用程序内存使用情况的更好估计。当大多数内存泄漏是同一类型的实例并且此类型仅在少数几个函数中实例化时,它们非常有用。当对象类型非常常见(例如 str
或 tuple
)时,问题就出现了,并且很难确定这些对象是在哪里实例化的。
查找引用循环也是一个难题。有不同的工具可以绘制所有引用的图表。这些工具不能用于具有数千个对象的大型应用程序,因为图表太大而无法手动分析。
提案
使用来自 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()
函数,一个环境变量(PYTHONFAULTHANDLER
和 PYTHONTRACEMALLOC
)以及一个 -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 帧。默认情况下,内存块的追踪仅存储最近的帧:限制为1
。nframe 必须大于或等于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
属性
如果inclusive为True
(包含),则仅跟踪在名称与filename_pattern
匹配的文件中第lineno
行分配的内存块。如果inclusive为
False
(排除),则忽略在名称与filename_pattern
匹配的文件中第lineno
行分配的内存块。
lineno
属性
过滤器的行号(int
)。如果lineno为None
,则过滤器匹配任何行号。
filename_pattern
属性
过滤器的文件名模式(str
)。
all_frames
属性
如果all_frames为True
,则检查回溯中的所有帧。如果all_frames为False
,则仅检查最近的帧。如果回溯限制小于
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_by和cumulative参数,请参阅
statistics()
方法。结果根据以下顺序排序:
StatisticDiff.size_diff
的绝对值、StatisticDiff.size
、StatisticDiff.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'
回溯 如果cumulative为
True
,则累加跟踪回溯中所有帧的内存块大小和数量,而不仅仅是最新的帧。累积模式只能与group_by等于'filename'
和'lineno'
以及traceback_limit
大于1
一起使用。结果根据以下顺序排序:
Statistic.size
、Statistic.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 相关工作。
链接
tracemalloc
版权
本文档已进入公有领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0454.rst