高级调试工具#
如果你到达这里,你想要深入或使用更高级的工具.这通常对于初次贡献者和大部份日常开发来说不是必需的.这些工具较少使用,例如在接近一个新的 NumPy 发布时,或者当进行一个大型或特别复杂的更改时.
由于并非所有这些工具都经常使用,并且仅在某些系统上可用,请预期差异、问题或怪癖;如果您遇到困难,我们很乐意帮助,并感谢对这些工作流程的任何改进或建议.
使用附加工具查找C错误#
大多数开发不需要比 调试 中显示的典型调试工具链更多.但例如内存泄漏可能特别微妙或难以缩小范围.
我们不期望大多数贡献者运行这些工具.然而,你可以确保我们能更容易地追踪这些问题:
测试应覆盖所有代码路径,包括错误路径.
尽量编写简短和简单的测试.如果你有一个非常复杂的测试,考虑创建一个额外的更简单的测试.这可能会有帮助,因为通常很容易找到哪个测试触发了问题,而不是测试中的哪一行.
如果数据被读取/使用,切勿使用
np.empty
.valgrind
会注意到这一点并报告错误.当你不关心值时,你可以生成随机值代替.
这将帮助我们在您的更改发布之前捕捉任何疏忽,这意味着您不必担心引用计数错误,这可能会让人望而生畏.
Python 调试构建#
Python 的调试版本很容易通过系统包管理器在 Linux 系统上获得,但也可以在其他平台上使用,可能以不太方便的格式提供.如果你无法通过系统包管理器轻松安装 Python 的调试版本,你可以使用 pyenv 自己构建一个.例如,要安装并全局激活 Python 3.10.8 的调试版本,可以这样做:
pyenv install -g 3.10.8
pyenv global 3.10.8
请注意,``pyenv install`` 从源代码构建 Python,因此您必须在构建之前确保安装了 Python 的依赖项,请参阅 pyenv 文档以获取特定于平台的安装说明.您可以使用 pip
安装调试会话中可能需要的 Python 依赖项.如果在 pypi 上没有可用的调试轮,您需要从源代码构建依赖项,并确保您的依赖项也编译为调试版本.
通常,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`` 支持”抑制”以忽略其中的一些,而 Python 确实有一个抑制文件(甚至是一个编译时选项),如果您发现有必要的话,这可能会有所帮助.
Valgrind 帮助:
查找未初始化变量/内存的使用.
检测内存访问违规(读取或写入分配内存之外的区域).
找到 许多 内存泄漏.注意,对于 大多数 泄漏,python 调试构建方法(和
pytest-leaks
)要敏感得多.原因是valgrind
只能检测内存是否绝对丢失.如果:dtype = np.dtype(np.int64) arr.astype(dtype=dtype)
对于
dtype
的引用计数不正确,这是一个错误,但 valgrind 无法看到它,因为np.dtype(np.int64)
总是返回相同的对象.然而,并不是所有的 dtype 都是单例,所以对于不同的输入,这可能会泄漏内存.在极少数情况下,NumPy 使用malloc
而不是 Python 内存分配器,这些分配器对 Python 调试构建是不可见的.通常应避免使用malloc
,但也有一些例外(例如,``PyArray_Dims`` 结构是公共 API,不能使用 Python 分配器.)
尽管使用 valgrind 进行内存泄漏检测速度较慢且敏感度较低,但它可以非常方便:你可以在不修改的情况下使用 valgrind 运行大多数程序.
需要注意的事项:
Valgrind 不支持 numpy 的
longdouble
,这意味着测试将失败或被标记为完全正常的错误.在运行你的 NumPy 代码之前和之后,预计会出现一些错误.
缓存可能意味着错误(特别是内存泄漏)可能不会被检测到,或者只在之后的不相关时间被检测到.
valgrind 的一大优势是它除了 valgrind 本身之外没有其他要求(尽管你可能希望使用调试构建以获得更好的回溯).
与 pytest
一起使用#
你可以使用 valgrind 运行测试套件,当你只对几个测试感兴趣时,这可能就足够了:
PYTHOMMALLOC=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 的示例命令).