让我们从一个公告开始:SciPy 现在在 Linux 上使用 Meson 构建,并且完整的测试套件通过了!这是一个非常令人兴奋的里程碑,对于 SciPy 维护者和贡献者来说是个好消息——他们可以期待更快的构建和更愉快的开发体验。那么它有多快?目前,在我 3 岁的 12 核 Intel CPU(i9-7920X @ 2.90GHz)上构建大约需要 1 分钟 50 秒(改进了约 4 倍):使用 Meson 对 SciPy 进行并行构建(12 个作业)的分析结果。使用 ninjatracing 和 Perfetto 创建的可视化。从跟踪结果中可以看出,构建单个 C++ 文件(bsr.cxx,这是 SciPy 的稀疏矩阵格式之一)需要超过 90 秒。因此,1 分 50 秒的构建时间接近最佳状态——改进它的唯一方法是对该 C++ 代码进行大手术,或者购买更快的 CPU。在 Python 3.10 中 distutils 将被弃用,在 Python 3.12 中它将被删除(参见 PEP 632)。 SciPyuses numpy.distutils 和 setuptools 作为其构建系统,因此从 Python 标准库中删除 distutils 将对 SciPy 产生重大影响 - 对直接扩展 distutils 的 numpy.distutils 影响更大。该 PEP 是大约一年前编写的,当时我的第一直觉是等到 setuptools 集成了 distutils(setuptools 维护者仍然计划这样做,并使用清理过的 API)然后更新 numpy.distutils 以进行更改。这还需要将 numpy.distutils 的部分移动到 setuptools 中,例如 Fortran 支持(有关更多详细信息,请参阅 setuptools/issues/2372)。很明显,这将是一个非常缓慢和痛苦的过程 - 将近一年后,供应商的 distutils 仍然没有在 setuptools 中重新启用。另一个驱动因素是 SciPy 的开发随着时间的推移变得更加痛苦。 SciPy 包含 C、C++、Fortran 和 Cython 代码,导出 C 和 CythonAPI,具有复杂的依赖关系(见下图)并进行大量代码生成。越来越多的 C++ 和 Cython 代码增加了构建时间。 CI 系统经常超时,因为构建和测试套件运行需要接近 60 分钟(几个免费的开源 CI 产品的限制)。编译代码的工作既麻烦又缓慢,以至于其他几个维护者已经说过我认为这是他们开发新功能的障碍。最后,调试构建问题非常困难——distutils 并没有真正的设计,所以其他每个扩展都只是根据需要修补。今年早些时候,我花了半天时间通过四个(!)不同的项目来寻找构建问题,这些项目都修改了扩展的构建方式。这些事情一起让我意识到是时候转向更好的构建系统了——无论如何,如果我们因为 PEP 632 而被迫做很多工作,那么现在是时候了。这就留下了一个问题:哪个构建系统?实际上只有两个可行的候选者:CMake 和 Meson。经过一些实验,我强烈偏爱 Meson,主要有两个原因:它有很好的文档(与 CMake 不同),并且很容易做出贡献(大约 25,000 行干净的 Python 代码,而 C/C++ 大约为 1,000,000 行)对于 CMake)。鉴于使用 CMake 或 Meson 的 Python 项目很少,好的文档和易于理解的代码库非常重要——我们将不得不做出贡献! CMake 现在通过 scikit-build 拥有更大的用户群,以及比 Meson 更好的 Python 集成。但对我来说那并不重要。此外,scikit-build 仍然依赖于 setuptools,本练习的目标之一是完全摆脱 distutils/setuptools,仅使用现代构建系统和 Python 打包标准(如 PEP 517、518 和 621)与 pip 和其他 Python 打包工具进行交互。
所以在二月份我写了一个 RFC,标题是“切换到 Meson 作为构建系统”,在积极的反馈之后,我开始着手这个项目。 SciPy 的外部构建和运行时依赖项。未显示供应商依赖项(例如,SuperLU、ARPACK、Boost、Uarray、HiGHS、Qhull、PocketFFT 等的大部分)。 $ time meson setup build... # 输出:~50 行有用的 configure inforeal 0m3.765suser 0m5.718ssys 0m4.456s$ time ninja -C build -j 12... # 输出:~25 行构建输出,仅相关编译器警告 [1311/1311] 链接目标 scipy/sparse/sparsetools/_sparsetools.cpython-39-x86_64-linux-gnu.soreal 1m48.058suser 19m58.007ssys 2m4.746s $ export BUIL12_NUM $ 并行导出文件.so$ time python setup.py build_ext --inplace -j 12 # 并行化 .pyx -> .c/cxx... # 输出:约 17,000 行构建输出,主要是噪音。 # 相关警告实际上不可能被发现。real 6m56.364suser 13m15.175ssys 0m39.141s 因此,Meson 构建速度大约快 4 倍。 numpy.distutils 慢的主要原因是并行性是有限的 - 很难修复阻止完全并行运行 build_ext 的竞争条件。另一个原因是它只是以一种相当临时的方式直接调用编译器,而 Meson 使用 Ninja 作为后端。 Ninja 几乎和它一样快。我们还可以看到,我们现在得到了一个干净的构建日志。这是花了一段时间才达到这一点的部分原因——每个编译器警告要么在 meson.build 文件中被静音,要么在 SciPy 的主分支中修复。一个非常好的改进是您实际上不必再运行构建来弄清楚扩展是如何构建的。要查看编译器标志、包含路径以及与目标构建方式相关的其他信息,只需运行 meson introspect build/ -i --targets 即可获得如下 JSON 表示:
{“名称”:“_sparsetools.cpython-39-x86_64-linux-gnu”,“id”:“c534037@@_sparsetools.cpython-39-x86_64-linux-gnu@sha”,“类型”:“共享模块” ,“defined_in”:“/home/rgommers/code/bldscipy/scipy/sparse/sparsetools/meson.build”,“文件名”:[“/home/rgommers/code/bldscipy/build/scipy/sparse/sparsetools/_sparsetools .cpython-39-x86_64-linux-gnu.so"], "build_by_default" : true , "target_sources" : [ { "language" : "cpp", "compiler" : [ "/home/rgommers/anaconda3/envs/ scipy-meson/bin/x86_64-conda-linux-gnu-c++”],“参数”:[“-I/home/rgommers/code/bldscipy/build/scipy/sparse/sparsetools/_sparsetools.cpython-39-x86_64 -linux-gnu.so.p", "-I/home/rgommers/code/bldscipy/build/scipy/sparse/sparsetools", "-I/home/rgommers/code/bldscipy/scipy/sparse/sparsetools", "-I/home/rgommers/anaconda3/envs/scipy-meson/lib/python3.9/site-packages/numpy/core/include" , "-I/home/rgommers/anaconda3/envs/scipy-meson/include /python3.9"、"-fdiagnostics-color=always"、"-D_FILE_OFFSET_BITS=64"、"-Wa ll", "-Winvalid-pch", "-Wnon-virtual-dtor", "-std=c++14", "-O2", "-g", "-fvisibility-inlines-hidden", "- std=c++17", "-fmessage-length=0", "-march=nocona", "-mtune=haswell", "-ftree-vectorize", "-fPIC", "-fstack-protector-strong ", "-fno-plt", "-O2", "-ffunction-sections", "-pipe", "-isystem", "/home/rgommers/anaconda3/envs/scipy-meson/include", "- DNDEBUG", "-D_FORTIFY_SOURCE=2", "-O2", "-isystem", "/home/rgommers/anaconda3/envs/scipy-meson/include", "-fPIC", "-DNPY_NO_DEPRECATED_API=NPY_1_9"] “来源”:[“/home/rgommers/code/bldscipy/scipy/sparse/sparsetools/bsr.cxx”,“/home/rgommers/code/bldscipy/scipy/sparse/sparsetools/csc.cxx”,“/home /rgommers/code/bldscipy/scipy/sparse/sparsetools/csr.cxx", "/home/rgommers/code/bldscipy/scipy/sparse/sparsetools/other.cxx", "/home/rgommers/code/bldscipy/scipy /sparse/sparsetools/sparsetools.cxx" ], "generated_sources" : [] } ], "extra_files" : [], "subproject" : null , "installed" : true , "install_filena me" : [ "/usr/local/lib/python3.9/site-packages/scipy/sparse/_sparsetools.cpython-39-x86_64-linux-gnu.so"] } 这有助于快速查明 meson.build 中的错误文件。此外,调试 Meson 本身的潜在问题,生成的 ninja.build 文件也相当可读 - 它的语法足够简单,可以轻松找到缺少的依赖项(例如,scipy.cluster 扩展依赖于 scipy.linalg.cython_linalg,并且该依赖项必须声明正确)。交叉编译将成为可能。多年来,我们一直告诉人们“抱歉,distutils 并不是真正为交叉编译而设计的,如果您有运气,请告诉我们”。因此,我们完全忽略了一些异国平台上的用户,并且还花了很多时间与不同的 CI 系统进行斗争以进行原生构建。例如,我们可能希望交叉编译为 aarch64,而不是在 Travis CI 上与动力不足的 ARM 硬件斗争。开发人员可以同时使用多个构建。因为 Meson 构建在设计上是树外的,所以现在很容易在同一个 repo 中并行地拥有例如 GCC 构建、Clang 构建和 Python 调试构建。更多的开发工具开箱即用,例如,如果安装了 ccache,它将被拾取,无需配置。我还设法让 AddressSanitizer 只进行了一些小的调整。构建定义更容易理解和修改。并非一切都更容易,但常见的任务如根据某些条件设置编译器标志(如“编译器支持此标志”)当然是: # meson.build thread_dep = dependency ('threads' , required : false ) if thread_dep 。 found () pocketfft_threads = '-DPOCKETFFT_PTHREADS' endif py3 。扩展模块('pypocketfft','pypocketfft.cxx',cpp_args:pocketfft_threads,include_directories:inc_pybind11,依赖项:[py3_dep,thread_dep],gnu_symbol_visibility:'hidden:',subcippff',subcippff安装,/
# setup.py def pre_build_hook ( build_ext , ext ): from scipy._build_utils.compiler_helper import ( set_cxx_flags_hook , try_add_flag , try_compile , has_flag ) cc = build_ext 。 _cxx_compiler args = ext 。 extra_compile_args set_cxx_flags_hook ( build_ext , ext ) 如果 cc 。 compiler_type == 'msvc' : args 。 append ( '/EHsc' ) else : # 如果可用,请使用 pthreads has_pthreads = try_compile ( cc , code = '#include <pthread.h> \n ' 'int main(int argc, char **argv) {} ' ) if has_pthreads:分机。定义宏。 append (( 'POCKETFFT_PTHREADS' , None )) 如果 has_flag ( cc , '-pthread' ): args 。追加('-pthread')ext。 extra_link_args 。 append ( '-pthread' ) else : raise RuntimeError ( "Build failed: System has pthreads header " "but could not be compile with -pthread option" ) # 不要导出库符号 try_add_flag ( args , cc , '-fvisibility=hidden ' ) def 配置 ( parent_package = '' , top_path = None ): ... config = Configuration ( '_pocketfft' , parent_package , top_path ) ext = config 。 add_extension ('pypocketfft',sources = ['pypocketfft.cxx'],depends = ['pocketfft_hdronly.h'],include_dirs = include_dirs,language = 'c++') ext。 _pre_build_hook = pre_build_hook Meson 是一个精心设计的构建系统,YouTube 上的文档和 Meson 开发人员的一些演讲都很好地解释了这种设计。所以我不会尝试在这里给出完整的图片。然而,当我开始这个项目时,很少有一些特别重要的事情可以帮助我完全掌握。让我们来看看这些。构建必须是树外的。 Meson 不允许在您现有的源代码树中构建 - 这包括代码生成。这有很好的理由,但是当来自 distutils 时,它可能会咬你。例如,如果你有一个脚本来生成 Cython .pyx 和 .pxd 文件(在 SciPy 中很常见),这些文件不能放在模板旁边,它们必须进入一个构建目录(我们用介子设置 <builddir> 选择的那个) .介子不是图灵完备的,也不能通过 API 进行扩展。这意味着如果 Meson 本身不支持某些东西,你不能仅仅编写一些 Python 代码来解决这个问题。相反,您必须将它添加到介子本身。如果这需要太长时间,只需 fork Meson 并使用您的 fork,直到您的功能在上游合并。这看起来很痛苦,但它保证人们不会只是在项目之间复制更改,长期可维护性会恶化。相反,其理念是为所有用户一次性解决问题。必须明确列出所有源文件和目标。这意味着如果您从 50 个 Fortran 文件构建单个 Python 扩展,则必须列出所有 50 个文件的名称。这在实践中并不是真正的问题,但它可能有点冗长。使用一些帮助程序片段在 IPython 中生成文件列表可以节省时间:Meson 有一个带有不可变对象的命令式 DSL。 DSL 的语法受 Python 启发,易于阅读。对象 - 可以是依赖项、生成的文件、构建的扩展等 - 不可变使事情易于调试,但在某些情况下,它会限制您可以做什么。例如,这段用于构建所有 scipy.sparse.csgraph 扩展的代码非常优雅,但是如果生成了 .pyx 文件,则 foreach 模式将不起作用。这是因为它们将是由 custom_target 创建的对象,并且没有语法可以在 foreach 循环中为它们提供唯一名称 - 这花了我一些时间来弄清楚: pyx_files = [ [ '_flow' , '_flow.pyx' ], [ '_matching' , '_matching.pyx' ], [ '_min_spanning_tree' , '_min_spanning_tree.pyx' ], [ '_reordering' , '_reordering.pyx' ], [ '_shortest_path' , '_shortest_path'],.pyx [ '_tools' , '_tools.pyx' ], [ '_traversal' , '_traversal.pyx' ], ] foreach pyx_file : pyx_files py3 。 extension_module ( pyx_file [ 0 ], pyx_file [ 1 ], include_directories : inc_np , dependencies : py3_dep , install : true , subdir : 'scipy/sparse/csgraph' ) endforeach
Meson 使用 pkg-config 来发现依赖关系。这意味着某些事情“正常工作”。例如,blas = dependency('openblas') 然后使用 dependencies: blas 来构建 scipy.linalg._fblas 扩展对我来说是第一次尝试 - 绝对没想到。有一个逃生舱可以做 Meson 本身不支持的事情:custom_target。只要列出它的源文件和输出,就可以通过 custom_target 调用 Python 脚本。这就是 SciPy 构建调用 numpy.f2py、Pythran 和 cython --cplus 的方式(Meson 的 Cython 支持是全新的,如果您现在将 .pyx 文件提供给 py3.extension_module ,则仅支持针对 C)。并非一切都一帆风顺。以下是我遇到的一些最重要的问题: Cython + 生成的源代码。这是迄今为止最耗时的话题。 Cython 的 cimport 机制依赖于完整的源代码树。例如,如果您正在构建的 .pyx 文件所在的两个目录没有 __init__.py 文件,则会更改 Cython 生成的 C 代码。完整的源代码树布局对 Cython 很重要。 Cython 主要围绕树内构建而设计,而 Meson 只允许您在树外生成文件。这需要像编写脚本或调用 cp 以复制许多 __init__.py 和 .pxi|.pxd 文件之类的技巧。然后进行更多的黑客攻击以确保 Ninja 将这些文件视为依赖项。 SciPy 是一团乱麻 - 有 17 个 scipy.xxx 子模块,几乎所有子模块都相互依赖。所以我们只能在 Meson 构建接近 100% 完成后开始运行测试。我们之前在 SciPy 中遇到过导入周期问题,现在我很惊讶我们过去没有更多问题。我在 Meson 中遗漏了一项功能:不允许使用 py3.install_sources 安装生成的文件。可以在 custom_target 中指定 install: true,但是要识别正确的 Python 特定安装目录有点麻烦,然后不可能生成 10 个文件并且只安装其中的两个(我遇到过多次,通常再次使用 Cython,我们在构建时需要 .pyx 和 .pxi 文件,在运行时也需要 .pxd 文件)。使用介子设置时必须使用 --prefix 。如果你不这样做,Meson 只会忽略你构建的 Python 解释器,并简单地安装到默认的 /usr/local/lib/,如果需要,请求提升权限。 Meson 不知道它正在构建一个 Python 包(我们刚刚在项目定义中告诉它我们正在构建 C、C++、Cython 和 Fortran 代码)所以从 Meson 的角度来看,这是正常的。这不是一个主要问题,但是我忘记经常添加 --prefix=$PWD/installdir 以至于它是一个摩擦点。计划是通过编写一个 dev.py CLI 包装器来解决这个问题,类似于 SciPy 的 runtests.py。
现在,介子支持存在于我的 SciPy 分支中。如果你想使用它,请参阅 rgommers/scipy/MESON_BUILD.md。在我们宣布胜利并使 Meson 成为默认构建系统之前,还有很多工作要做。最重要的主题是: 当我开始这项工作时,我还不确定使用 Meson 是否会奏效。但是我确实知道,如果构建 SciPy 成功,Meson 几乎可以用于任何其他科学 Python 项目。在 distutils 消失之前我们还有大约两年的时间,我希望我们可以在那个时间段内停用 numpy.distutils。没有复杂构建要求的项目(即只有 Cython,或者可能是一些 C 扩展)应该可以简单地使用 setuptools。那些具有复杂构建的项目可以转移到 Meson,或者如果他们更喜欢 CMake,则可以转移到 scikit-build。有很多技术原因可以支持或反对使用任何构建系统。然而,为什么我确信 Meson 是 SciPy 和其他项目的绝佳选择是:Meson 使用起来很愉快。它“感觉不错”。这是关于构建系统的罕见说法 - 我当然没有听到任何人这样说 numpy.distutils 或 setuptools。也不关于 CMake。在 NumPy 和 SciPy 之前,我们有两个其他替代构建系统:首先是 NumScons(基于 Scons),然后是 Bento(基于 Waf)。两者都是由 DavidCournapeau 创建的; 2008 年的 NumScons 和 2011 年的 Bento。Bento/Waf 与 Meson 最相似,它使在 SciPy 上的工作变得更好,我将其维护到 2018 年 - 但它从未过渡到 Python 3。是的,我一直使用 Python 2.7 直到 2018 年特别是这样我就可以使用 Bento 而不是 distutils - Python 3.x 中没有什么有趣的地方值得每天处理 distutils。不幸的是,Waf 不是一个足够稳定的项目来构建,而 Bento 是一个人的项目。 Meson 甚至比 Waf 更好,并且维护良好,因此我们终于可以拥有一个不错的构建系统。这个项目耗费了很多晚上和周末才走到这一步。如果没有一些非常重要的贡献,我不会在五个月内走得这么远。 Dylan Baker 是 Meson 的维护者之一,在我的“将 Cython 视为一种语言?”之后,很快就在 Meson 中实现了对 Cython 的支持,令人印象深刻。提议。 ......