Arrow 是一种跨语言的、标准化的“列式内存数据规范”。

在 Arrow 出现之前,大数据和机器学习生态里有一个巨大的痛点,我们姑且称之为“序列化税”。假设你用 Python 的 Pandas 处理完一个巨大的 DataFrame,想把它交给底层的 C++ 引擎(比如 XGBoost)或者 JVM 上的 Spark 去跑。Pandas 在内存里的数据布局,和 C++ 里的 std::vector 或 Spark 里的 InternalRow 是完全不一样的。

怎么办?只能把 Pandas 的数据从内存里拆出来,序列化成 CSV、JSON 或者 Parquet 写到磁盘(或者通过网络发过去),然后 C++ 引擎再把它反序列化读进自己的内存里。这一来一回,哪怕数据量只有几个 GB,也能让你的 CPU 跑满,耗时几十秒。这在交互式分析和复杂数据管线里是致命的。

Arrow 的出现就是为了终结这种荒谬的浪费。它定义了一套所有语言、所有引擎都必须遵守的“内存布局标准”。只要大家都按 Arrow 的规矩在内存里摆放数据,Pandas 就可以直接把自己的内存指针交给 C++,C++ 拿着指针直接读,中间没有任何拷贝和序列化,这就是所谓的 Zero-copy(零拷贝)。Arrow 就是现代数据基础设施里的“通用语”。

顺便提个技术圈的八卦,Arrow 项目的发起人之一是 Wes McKinney,也就是 Pandas 的作者。他当年就是因为受够了 Pandas 在内存管理和跨语言调用上的局限性,才愤而牵头搞了 Arrow。

为什么叫“Arrow”(箭头)?

Apache 基金会的项目起名通常很有讲究,Arrow 这个名字既有工程意象,也带着点技术底层的隐喻。

从宏观愿景上看,Arrow(箭)代表着速度和穿透力。它旨在像利箭一样,穿透不同编程语言、不同计算引擎之间的壁垒,让数据无缝流转。

但如果深入到 Arrow 的底层内存结构,你会发现这个名字更加写实。前面我们提到 Arrow 是列式存储,对于定长数据(比如 int32),内存布局很简单,就是一块连续的数组。但对于变长数据(比如字符串 string,或者列表 list<int>),你怎么在连续内存里存放长度不一的东西?

Arrow 的做法是:开辟一块连续的巨大字节缓冲区(Buffer)用来存所有字符串的实际内容,然后再开辟一个连续的整数数组(Offsets)用来记录每个字符串的起始位置。这个 Offsets 数组里的每一个整数,就像是一个 “箭头”(Arrow),指向底层 Buffer 中的具体位置。这种通过偏移量指针来管理变长数据的设计,是 Arrow 内存格式的核心,叫它 Arrow 也就顺理成章了。

Arrow 和 parquet 是什么关系?

这是工程实践里最容易被混淆的一对概念。很多初学者觉得既然都是列式存储,它俩是不是同一个东西的不同版本?

其实不是。它们解决的是完全不同维度的问题:Arrow 是内存格式(In-Memory),Parquet 是磁盘格式(On-Disk)。

你可以用一个比喻来理解。Parquet 就像是图书馆里经过极度压缩、编好目录、锁在保险柜里的档案。它的设计目标是“持久化”和“省空间”,所以它里面有大量的压缩算法(Snappy、Gzip)、有字典编码、有极其复杂的页(Page)结构,但它不适合直接拿来做加减乘除。

而 Arrow 就像是你工作台上的草稿纸。它的设计目标是“好算”。它不压缩(解压太费 CPU),它的内存布局是为了让 CPU 的 SIMD(单指令多数据流)指令集能一次性处理多个数据而优化的,它就是为了被 CPU 直接咀嚼而生的。

这两者是天然的黄金搭档。一个极其经典的现代数据管线是这样工作的:你把海量数据以 Parquet 的格式便宜、紧凑地存在磁盘或 S3 上;当需要计算时,系统把 Parquet 文件按需读取,在内存中将其解压并展开成 Arrow 格式,然后交给计算引擎狂算;算完之后,再把结果从 Arrow 转回 Parquet 存起来。

在 HuggingFace 的 datasets 包里,当你调用 load_dataset("parquet", ...) 时,底层发生的事情正是如此。datasets 利用了操作系统的内存映射(mmap)技术,把磁盘上的 Parquet 文件映射到虚拟内存,当你迭代数据时,它按需将磁盘上的 Parquet 页动态转换为 Arrow 内存块喂给 PyTorch。这就解释了为什么它既不占满物理内存,又能提供极快的读取速度。