简化 Arrow C++ 构建,使其更小、更快
已发布 2020年7月29日
作者 The Apache Arrow PMC (pmc)
在过去的四年半中,我们一直致力于构建一个“包含所有必要工具”的开发平台,用于 C++ 中的高性能分析应用程序。 随着项目范围的扩大,我们有时会增加额外的库依赖项,以支持各种系统和数据处理任务。
虽然这些依赖项为我们解决难题提供了帮助,但在某些情况下,它们也增加了依赖 Arrow 的项目的复杂性。 因此,一些项目一直担心依赖 Arrow C++ 库,特别是如果他们对 Arrow 库的功能的使用有限。 实际上,在 Arrow 项目开发的早期阶段,依赖项管理问题确实给早期采用者带来了问题。
我们希望开发人员相信他们可以使用和依赖我们的库,并且这样做不会增加他们自己的项目维护或用户的负担。 在过去的一年中,我们进行了一些重要的项目,以适应人们希望以不同方式依赖 Arrow C++。 我们的目标是使构建过程默认情况下简单,无需特殊的环境设置,但对于需要专业化的人来说,也可以高度配置。 这包括一个零依赖项选项,适用于希望使用 Arrow C++ 核心但不承担任何传递依赖项的项目。 即使我们继续添加新功能,我们也一直在努力使构建更快、更紧凑。
这篇文章涵盖了我们在 C++ 库以及依赖它们的 Arrow Python 和 R 包中所做的许多努力。 与一年前相比,构建体验在更广泛的平台上更加可靠,需要的依赖项更少,并且产生的软件包更小。
最小的默认构建选项
对于将 Arrow 用作依赖项的人来说,一个粗糙的地方是,默认情况下在构建中启用了许多可选的项目组件,因此需要这些可选组件的任何额外依赖项。 我们没有期望用户逐个禁用可选组件,而是将所有可选组件的默认设置为 OFF
,以便默认配置是无依赖项的最小核心构建。
默认情况下启用的唯一第三方库是 jemalloc,该项目推荐的内存分配器(在 Windows 上除外,它也被禁用)。 鉴于 Arrow 应用程序通常处理大量数据,我们还发现,使用像 jemalloc 和 mimalloc 这样的项目提供的内存分配器比默认系统分配器产生显着更好的性能。 即便如此,如果需要,也可以禁用此选项。
为了演示最小构建,我们提供了一个 Dockerfile,该文件可用于构建项目,只需要 CMake 和一个 C++ 编译器,并且没有依赖项。 此外,我们还包含了一个 示例,用于在另一个 CMake 项目中将 Arrow 作为外部项目依赖项包含在内。
CMake 中灵活的依赖项配置
作为改进基于 CMake 的构建系统的一部分,我们使构建依赖项的配置对于不同用户的需求既灵活又一致。 在某些情况下,开发人员希望 Arrow 根据外部软件包管理器(例如基于 Debian 的 Linux 发行版中的 apt)提供的依赖项进行构建。 在其他情况下,开发人员可能希望避免系统库的任何怪癖,并将所有依赖项与 Arrow 构建一起构建。
对于每个软件包,可以将 ${Library}_SOURCE
CMake 选项设置为以下三个值之一
SYSTEM
,当依赖项要从外部提供时(例如,通过 Linux 发行版或 Homebrew)BUNDLED
,当您希望在构建 Arrow 时从源代码构建依赖项,然后将该依赖项与生成的库进行静态链接时AUTO
,它尝试SYSTEM
方法,但如果找不到依赖项,则回退到BUNDLED
我们还为开发人员使用 conda 或 Homebrew 软件包管理器时的常见情况提供了 CONDA
和 BREW
源类型。 可以根据单个依赖项或使用 ARROW_DEPENDENCY_SOURCE
CMake 选项全局配置这些依赖项源。 AUTO
是默认值,它可以通过尽可能使用预构建的系统库来加快构建速度,但即使系统上没有所有依赖项,仍然可以成功。
减少外部依赖项
另一个关注的领域是审核我们的依赖项。 我们进行了检查,并找到了可以删除外部依赖项而不会丢失有用功能,也不必重写大量代码或将太多代码复制到我们的代码库中的位置。
我们已经消除了 Boost 作为核心 Arrow 库的依赖项,并且在其他组件(Gandiva、Parquet 等)中,Boost 的使用已大大减少。 此外,在 Arrow 构建中“捆绑”构建 Boost 时,我们剥离了我们下载的 Boost 包,使其仅为所需的最小值,从而减少了 90% 的下载大小。
我们提供了一些小的依赖项,例如 double-conversion 和 uriparser 库,因此它们无需单独下载和构建。
我们还编译了 Flatbuffers 和 Thrift 定义(这是实现 Arrow 和 Parquet 格式所必需的),并将生成的 C++ 代码检入到 Arrow 存储库中。 这意味着 Flatbuffers 不再是 Arrow 的构建或运行时依赖项,我们只需要 Thrift C++ 库,而不需要 Thrift 编译器,该编译器对 flex 和 bison 有额外的依赖项。
C++ 库大小减少
随着 C++ 代码库的大小增长,编译时间和 C++ 编译器生成的二进制代码量也会随之增加。 在过去的几个月中,我们已经开始分析 Arrow 库的编译时间和生成的代码大小。 这既带来了显着的大小减少(自 0.17.0 以来,代码大小减少了 30% 以上)。 我们还重新构造了头文件,以避免包含不必要的头文件,从而减轻了 C++ 编译器的负担并缩短了编译时间。
Python wheels
在 Python 软件包索引 (PyPI) 上对二进制 wheel 软件包的期望是,它们是独立的并且没有外部依赖项,除非依赖于其他 Python 软件包。 此外,每个 pyarrow 用户可能需要来自项目的不同内容。 一些用户只想读取 Parquet 文件并将它们转换为 pandas 数据帧,而另一些用户则想使用 Flight 来移动大型数据集。 因此,从项目一开始,“pyarrow” wheel 就是一个相当全面的构建,包括我们可以维护的尽可能多的可选组件。
全面的 wheel 软件包有一些缺点:最值得注意的是对于用户来说,它很大。 此外,通过与 C++ 共享库相关的混乱,在几个版本中,wheel 软件包会在磁盘上创建每个 C++ 库的两个副本,从而导致磁盘使用量增加一倍。 这给在像 AWS Lambda 这样的空间受限的环境中使用 pyarrow 的人带来了问题。
在 1.0.0 版本中,我们实施了一些更改,这些更改使 wheel 的大小(以 .whl
形式和安装在磁盘上)减少了约 75%
- 解决导致在 site-packages 目录中创建每个共享库的两个副本的问题。
- 禁用 Gandiva,它需要 LLVM 运行时,这是最大的静态链接依赖项。 Gandiva 现在仍然可供 conda 用户使用 - 它只是不包含在 wheel 中 - 我们希望将来将其打包为单独的
pyarrow-llvm
软件包。 - 如上所述,减小 C++ 共享库的大小
现在,pyarrow 的大小与 NumPy 大致相同,因此 Python 项目更容易将其作为硬依赖项,而无需担心磁盘上的大尺寸。
展望未来,我们已经讨论了将 pyarrow 分解为多个 wheel 软件包的 策略,有点像“轮毂和辐射”模型,其中一些可选部分作为单独的 wheel 安装,因此只需要某些“核心”功能的人只需要安装一个小软件包。 不过,这将是一个重要的项目,因此目前我们专注于改进全面的 wheel 软件包。
R 软件包
为 R 打包 Arrow 涉及与 Python wheel 类似的挑战,尽管技术细节是独一无二的。 就像 pip install pyarrow
应该在任何地方都能正常工作一样,R 中的 install.packages("arrow")
也应该如此,并且我们已经投入了大量的精力来实现这一目标。 因为 R 软件包依赖于处于活跃开发中的 C++ 库,所以这并非易事,尤其是对于 Linux 上 C++ 编译器和标准库的所有组合。
在去年发布的初始 CRAN 版本 0.14.1 中,只有 Windows 和 macOS 的二进制包可以直接使用。对于 Linux,您必须先单独安装 C++ 库,然后才能安装 R 包。虽然 Python wheels 包含 Linux 上的二进制库,但 CRAN 只托管源代码包,这些源代码包必须在安装时在用户的机器上编译。这导致 Linux 用户体验不佳。
从 0.16 版本开始,在 Linux 上安装源代码包会自动处理其 C++ 依赖项。默认情况下,该软件包执行一个捆绑脚本,该脚本下载并构建 Arrow C++ 库,除了 R 需要的依赖项之外,没有任何系统依赖项。在许多常见的 Linux 发行版和版本上,可以通过设置环境变量来加速这一过程,以下载预构建的静态 C++ 库以包含在软件包中。
为了配合这些改进并确保它们在各种平台上取得成功,我们添加了大量的 每晚构建到我们的持续集成系统中。这些也是很容易扩展的 - 我们只需要一个包含 R 的 Docker 镜像,就可以将新的环境插入到我们的常规每晚测试中。
从那以后,我们继续改进安装体验,并寻找减少构建时间和包大小的方法。上面讨论的 C++ 库的改进有助于 R 包,因为大多数 R 包的安装要么构建要么包含 C++ 库。在 R 包本身中,我们一直在寻找只包含所需内容而不是更多内容的方法。这些努力带来了更小的下载量和已安装的软件包大小。从 0.17.1 到 1.0.0,macOS 和 Windows CRAN 二进制文件的已安装库大小减少了 10%,Linux 的预构建静态 C++ 库比 0.16.0 小 33%,尽管增加了许多新功能。
C 接口
最后,我们观察到,有些项目可能希望生成或使用 Arrow 格式的子集,并且不希望承担任何额外的代码依赖项。还有一些情况下,两个库需要共享内存中的 Arrow 数据结构,但无法依赖于公共的 Arrow 库,例如参考 C++ 实现。为了解决这些用例,我们设计了C 接口,以提供一种轻量级的方式,在 C 级别交换 Arrow 数据,而无需任何内存复制。
当使用 C 接口时,开发人员会填充简单的 C 数据结构,这些结构包含有关 Arrow 数据结构的模式(数据类型)信息以及构成数据的内存片段的地址。这允许库在内存中轻松地连接在一起,而无需任何共享代码(除了 C 接口结构定义)。大多数编程语言都能够操作 C 结构,因此即使不必编写或编译 C 代码也可以使用此接口。我们使用 C 接口在内存中使用 reticulate
在 Python 和 R 之间传输数据结构。
Arrow C 接口的一个令人兴奋的用例是将 Arrow 导入和导出添加到数据库驱动程序库,这些库通常包含 C API。
展望未来
随着项目的增长,我们将继续努力使构建过程尽可能快速和可靠。如果您发现我们可以进一步改进的方法,或者您遇到问题,请在我们的邮件列表或报告问题中提出。