高级调试工具#
如果您来到这里,说明您想深入了解或使用更高级的工具。对于初次贡献者和大多数日常开发来说,这通常不是必需的。这些工具使用频率较低,例如在新版 NumPy 发布前夕,或者在进行了大规模或特别复杂的更改时。
由于并非所有这些工具都经常使用,并且只在某些系统上可用,因此请预料到可能存在差异、问题或怪癖;如果您遇到困难,我们将很乐意提供帮助,并感谢您对这些工作流程的任何改进或建议。
使用额外工具查找 C 错误#
大多数开发不需要超出 调试 中所示的典型调试工具链。但例如内存泄漏可能特别微妙或难以缩小范围。
我们不期望大多数贡献者运行这些工具中的任何一个。但是,您可以确保我们更容易地追踪这些问题
测试应覆盖所有代码路径,包括错误路径。
尽量编写简短简单的测试。如果您的测试非常复杂,请考虑额外创建一个更简单的测试。这会很有帮助,因为通常很容易找到是哪个测试触发了问题,而不是测试的哪一行。
如果数据被读取/使用,切勿使用
np.empty
。valgrind
会注意到这一点并报告错误。当您不关心值时,可以生成随机值代替。
这将帮助我们在您的更改发布之前发现任何疏忽,这意味着您不必担心出现引用计数错误,这可能会令人望而生畏。
Python 调试构建#
Python 调试构建版本很容易获得,例如通过 Linux 系统上的包管理器,但它们也在其他平台上可用,可能以不太方便的格式。如果您无法通过系统包管理器轻松安装 Python 的调试构建版本,您可以使用 pyenv 自己构建一个。例如,要安装并全局激活 Python 3.13.3 的调试构建版本,可以这样做
pyenv install -g 3.13.3
pyenv global 3.13.3
请注意,pyenv install
从源代码构建 Python,因此在构建之前必须确保已安装 Python 的依赖项,请参阅 pyenv 文档以获取平台特定的安装说明。您可以使用 pip
安装调试会话可能需要的 Python 依赖项。如果 pypi 上没有调试轮子 (debug wheel) 可用,您将需要从源代码构建依赖项,并确保您的依赖项也编译为调试版本。
通常,Python 的调试构建版本将 Python 可执行文件命名为 pythond
而不是 python
。要检查您是否安装了 Python 的调试构建版本,您可以运行例如 pythond -m sysconfig
来获取 Python 可执行文件的构建配置。调试构建版本将在 CFLAGS
中使用调试编译器选项(例如 -g -Og
)进行构建。
运行 Numpy 测试或交互式终端通常很简单,只需
python3.8d runtests.py
# or
python3.8d runtests.py --ipython
并且已在 调试 中提及。
Python 调试构建将有所帮助
查找可能导致随机行为的错误。一个例子是对象被删除后仍在使用的情况。
Python 调试构建允许检查正确的引用计数。这通过使用额外命令来实现
sys.gettotalrefcount() sys.getallocatedblocks()
Python 调试构建允许使用 gdb 和其他 C 调试器进行更轻松的调试。
与 pytest
结合使用#
仅使用 Python 调试构建来运行测试套件本身不会发现很多错误。Python 调试构建的另一个优点是它允许检测内存泄漏。
一个简化此过程的工具是 pytest-leaks,可以使用 pip
进行安装。不幸的是,pytest
本身可能会泄漏内存,但通常(目前)可以通过移除以下内容来获得良好的结果
@pytest.fixture(autouse=True)
def add_np(doctest_namespace):
doctest_namespace['np'] = numpy
@pytest.fixture(autouse=True)
def env_setup(monkeypatch):
monkeypatch.setenv('PYTHONHASHSEED', '0')
来自 numpy/conftest.py
(这可能会随着新的 pytest-leaks
版本或 pytest
更新而改变)。
这允许方便地运行测试套件或其中一部分
python3.8d runtests.py -t numpy/_core/tests/test_multiarray.py -- -R2:3 -s
其中 -R2:3
是 pytest-leaks
命令(请参阅其文档),-s
会导致输出打印并可能需要(在某些版本中,捕获的输出被检测为内存泄漏)。
请注意,有些测试已知(甚至设计成)会泄漏引用,我们尝试标记它们,但请预料到会有一些误报。
valgrind
#
Valgrind 是一个强大的工具,用于查找某些内存访问问题,应在复杂的 C 代码上运行。 valgrind
的基本使用通常只需要
PYTHONMALLOC=malloc valgrind python runtests.py
其中 PYTHONMALLOC=malloc
是必要的,以避免来自 Python 本身的误报。根据系统和 valgrind 版本,您可能会看到更多误报。 valgrind
支持“抑制 (suppressions)”以忽略其中一些,Python 确实有一个抑制文件(甚至一个编译时选项),如果您认为有必要,这可能会有所帮助。
Valgrind 有助于
查找未初始化变量/内存的使用。
检测内存访问违规(读写分配内存之外的区域)。
查找许多内存泄漏。请注意,对于大多数内存泄漏,Python 调试构建方法(和
pytest-leaks
)要敏感得多。原因是valgrind
只能检测内存是否确定丢失。如果dtype = np.dtype(np.int64) arr.astype(dtype=dtype)
对
dtype
的引用计数不正确,这是一个错误,但 valgrind 无法看到它,因为np.dtype(np.int64)
总是返回相同的对象。然而,并非所有 dtype 都是单例(singleton),因此这可能会导致不同输入的内存泄漏。在极少数情况下,NumPy 使用malloc
而不是 Python 内存分配器,后者对 Python 调试构建是不可见的。malloc
通常应避免使用,但也有一些例外(例如PyArray_Dims
结构是公共 API,不能使用 Python 分配器)。
尽管使用 valgrind 进行内存泄漏检测速度较慢且不那么敏感,但它很方便:您可以在不修改大多数程序的情况下运行 valgrind。
注意事项
Valgrind 不支持 numpy 的
longdouble
,这意味着测试会失败或被标记为错误,而实际上它们完全正常。预计在运行 NumPy 代码之前和之后会出现一些错误。
缓存可能意味着错误(特别是内存泄漏)可能不会被检测到,或者只在稍后不相关的时间才被检测到。
valgrind 的一个巨大优势是,除了 valgrind 本身之外,它没有其他要求(尽管您可能希望使用调试构建以获得更好的回溯信息)。
与 pytest
结合使用#
您可以使用 valgrind 运行测试套件,这可能在您只对少数测试感兴趣时足够了
PYTHONMALLOC=malloc valgrind python runtests.py \
-t numpy/_core/tests/test_multiarray.py -- --continue-on-collection-errors
请注意 --continue-on-collection-errors
,由于缺少 longdouble
支持导致失败,目前这是必需的(如果您不运行完整的测试套件,通常不需要此选项)。
如果您希望检测内存泄漏,您还需要 --show-leak-kinds=definite
以及可能的更多 valgrind 选项。就像 pytest-leaks
一样,某些测试已知会导致 valgrind 中出现泄漏错误,并且可能已标记或未标记。
我们开发了 pytest-valgrind,它可以
单独报告每个测试的错误
将内存泄漏缩小到单个测试(默认情况下,valgrind 只在程序停止后检查内存泄漏,这非常繁琐)。
请参阅其 README
获取更多信息(其中包含 NumPy 的示例命令)。