介绍 Apache Arrow C 数据接口
发布于 2020 年 05 月 03 日
作者 Antoine Pitrou (apitrou)
Apache Arrow 包含一个跨语言、平台无关的内存中列式格式,允许在异构运行时和应用程序之间进行零拷贝数据共享和传输。
使用 Arrow 列式格式最简单的方法一直是依赖 Apache Arrow 社区开发的具体实现之一。该项目代码库目前包含 11 种不同编程语言的库,未来可能会扩展到包括更多语言。
然而,有些项目可能希望在不引入新的库依赖(例如 Arrow C++ 库)的情况下导入和导出 Arrow 列式格式。因此,我们设计了一种替代方案,在 C 级别交换数据,遵循简单的数据定义。C 数据接口除了依赖于使用它的二进制文件之间共享的 C ABI 外,不依赖任何其他东西。C ABI 是跨平台的标准,所有生成二进制文件的编译器都必须遵守,并且极其稳定,确保了库和可执行二进制文件的可移植性。利用 C 数据接口定义的 C 结构体的两个库可以在运行时进行零拷贝数据传输,而无需任何构建时或链接时依赖要求。
了解 C 数据接口的最佳方法是阅读规范。不过,我们将快速回顾一下它的优点。
两个简单的结构体定义
要在 C 或 C++ 级别与 C 数据接口交互,您需要在代码中包含的唯一内容是两个结构体类型的声明(以及几个表示常量值的 #define)。这些声明只依赖于标准的 C 类型,可以简单地粘贴到头文件中。其他语言也可以参与,只要它们提供外部函数接口(Foreign Function Interface, FFI)层;大多数现代语言都是这种情况,例如 Python(使用 ctypes 或 cffi)、Julia、Rust、Go 等。
零拷贝数据共享
C 数据接口通过内存指针传递 Arrow 数据缓冲区。因此,从构造上讲,它允许您在不复制数据的情况下,在一个运行时到另一个运行时共享数据。由于数据采用标准的Arrow 内存格式,其布局是明确定义的,没有歧义。
此设计也将 C 数据接口限制为进程内数据共享。对于进程间通信,我们建议使用 Arrow IPC 格式。
减少编组
C 数据接口保持了在 C 或 C++ 中以自然方式表达类 Arrow 数据的方式。只有两个方面涉及非平凡的编组:
- 使用非常简单的基于字符串的语言对数据类型进行编码
- 使用非常简单的长度前缀格式对可选元数据进行编码
独立的数据类型和数据表示
对于生成大量单一数据类型实例的应用程序(例如,作为一系列记录批次),重复地从其字符串编码重建数据类型会带来不必要的开销。为了解决这种情况,C 数据接口定义了两个独立结构:一个表示数据类型(和可选元数据),一个表示一块数据。
生命周期处理
在异构运行时之间共享数据的一个常见难题是如何正确处理数据的生命周期。C 数据接口允许生产者通过一个释放回调定义自己的内存管理方案。这是一个简单的函数指针,消费者在使用完数据后会调用它。例如,当用作生产者时,Arrow C++ 库会传递一个释放回调,该回调会简单地递减 shared_ptr 的引用计数。
应用场景:在 R 和 Python 之间传递数据
R 和 Python 的 Arrow 库都基于 Arrow C++ 库,但它们各自的工具链(由 R 和 Python 打包标准规定)是 ABI 不兼容的。因此,不可能在 R 和 Python 绑定之间直接在 C++ 级别传递数据。
使用 C 数据接口,我们规避了这一限制,并在 R 和 Python 之间提供了零拷贝数据共享 API。它基于 R 的 reticulate 库。
这是一个混合使用 R 和 Python 库调用的示例会话
library(arrow)
library(reticulate)
use_virtualenv("arrow")
pa <- import("pyarrow")
# Create an array in PyArrow
a <- pa$array(c(1, 2, 3))
a
## Array
## <double>
## [
## 1,
## 2,
## 3
## ]
# Apply R methods on the PyArrow-created array:
a[a > 1]
## Array
## <double>
## [
## 2,
## 3
## ]
# Create an array in R and pass it to PyArrow
b <- Array$create(c(5, 6, 7))
a_and_b <- pa$concat_arrays(r_to_py(list(a, b)))
a_and_b
## Array
## <double>
## [
## 1,
## 2,
## 3,
## 5,
## 6,
## 7
## ]