高级调试工具#

如果你到达这里,你想要深入或使用更高级的工具.这通常对于初次贡献者和大部份日常开发来说不是必需的.这些工具较少使用,例如在接近一个新的 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:3pytest-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 的示例命令).