Apache Arrow C++ 数组机制详解:Array、ArrayBuilder 构建、ChunkedArray 与零拷贝 Slice
2026/9/14 11:02:48 网站建设 项目流程

Apache Arrow C++ 数组机制详解:Array、ArrayBuilder 构建、ChunkedArray 与零拷贝 Slice

【免费下载链接】arrowApache Arrow is the universal columnar format and multi-language toolbox for fast data interchange and in-memory analytics项目地址: https://gitcode.com/GitHub_Trending/arrow3/arrow

本文以 Apache Arrow C++ 官方文档 docs/source/cpp/arrays.rst 为核心,系统讲解 Arrow 中数组(arrow::Array)的内存布局、通过ArrayBuilder系构建数组的完整流程与性能优化手段(Reserve/Resize/UnsafeAppend)、ChunkedArray分块逻辑序列、零拷贝Slice以及面向示例与测试的*FromJSONString辅助函数。读完后,你将能够独立编写 C++ 代码高效地构建、访问和分片 Arrow 数组,并理解每一行示例背后的缓冲区(Buffer)语义与源码级实现。

1. 核心类型:arrow::Array 及其内存布局

Arrow 体系中的中心类型是 arrow::Array。一个数组表示已知长度、同一数据类型的值的序列。其内部由一个或多个arrow::Buffer构成,缓冲区的数量与含义完全取决于数组的数据类型,遵循 Arrow 数据布局规范(Format Layout Specification)。

这些缓冲区包含两部分内容:

  • 值数据本身:按类型布局存放(如定长数值类型的裸数据缓冲区);
  • 可选的 null bitmap(有效位图):标记哪些条目为 null。如果确定数组中没有 null 值,该位图缓冲区可以整个省略——这是 Arrow 布局的一个重要特性。

对于每种数据类型,arrow::Array都有具体的子类,帮助访问单个值。例如Int64Array本质上只是arrow::NumericArray<Int64Type>的便捷 typedef。

构建数组的两种总体策略

Arrow 对象是不可变的(immutable),不能像std::vector那样直接填充。因此共有两类策略:

  1. 数据已在内存中就绪:若数据已按 Arrow 布局存在于内存中,可将内存包装进arrow::Buffer实例,再构造描述该数组的arrow::ArrayData(涉及内存管理时可参考官方 memory 文档 docs/source/cpp/memory.rst);
  2. 增量构建:使用基类 arrow::ArrayBuilder 及其具体子类,逐值或批量地构建数组数据,无需自己处理 Arrow 格式细节。

此外,对于示例或测试等对性能不敏感的场景,可以直接使用*FromJSONString辅助函数,用 JSON 文本简写快速创建数组(见本文第 5 节)。

2. 使用 ArrayBuilder 及其子类

以构建一个Int64类型的 Arrow 数组为例,可以使用arrow::Int64Builder。下面的例子构建 1 到 8 的序列,其中应存放 4 的第四个位置为 null:

arrow::Int64Builder builder; builder.Append(1); builder.Append(2); builder.Append(3); builder.AppendNull(); builder.Append(5); builder.Append(6); builder.Append(7); builder.Append(8); auto maybe_array = builder.Finish(); if (!maybe_array.ok()) { // ... do something on array building failure } std::shared_ptr<arrow::Array> array = *maybe_array;

构建完成后,得到的 Array(可向下转型为具体的arrow::Int64Array以便访问值)由两个arrow::Buffer组成:

  • 第一个 buffer 保存 null bitmap,此处仅 1 个字节,位模式为1|1|1|1|0|1|1|1。由于采用最低有效位(LSB)编号,这表示数组的第四个条目为 null;
  • 第二个 buffer 就是存放上述全部值的int64_t数组。因为第四个条目是 null,该位置在 buffer 中的值是无定义的(undefined)。

访问具体数组内容

// Cast the Array to its actual type to access its data auto int64_array = std::static_pointer_cast<arrow::Int64Array>(array); // Get the pointer to the null bitmap const uint8_t* null_bitmap = int64_array->null_bitmap_data(); // Get the pointer to the actual data const int64_t* data = int64_array->raw_values(); // Alternatively, given an array index, query its null bit and value directly int64_t index = 2; if (!int64_array->IsNull(index)) { int64_t value = int64_array->Value(index); }

源码层面的对应关系:arrow::Int64Array分别是arrow::NumericArray<Int64Type>(及arrow::NumericBuilder<Int64Type>)的 typedef,提供便捷访问接口。Builder 侧的类型别名定义可见 builder_primitive.h:

using Int64Builder = NumericBuilder<Int64Type>;

Finish 的语义:返回 Result 并重置 Builder

从基类 builder_base.h 可以看到,Finish有两种重载:

/// The builder is reset except for DictionaryBuilder. Status Finish(std::shared_ptr<Array>* out); Result<std::shared_ptr<Array>> Finish();

注意注释:DictionaryBuilder外,Finish 会重置 builder——这正是下节ChunkedArray示例中可以复用同一个 builder 构建第二个 chunk 的原因。FinishInternal则返回内部通用的ArrayData对象,供嵌套类型 builder 组合使用。

3. 性能优化:Reserve、Resize、AppendValues 与 UnsafeAppend

虽然可以像上文示例那样逐值构建,但要获得最高性能,推荐使用具体arrow::ArrayBuilder子类中的批量追加方法(通常名为AppendValues)。如果预先知道元素数量,还推荐通过arrow::ArrayBuilder::Resizearrow::ArrayBuilder::Reserve方法预置工作区。

改写后的批量版本示例:

arrow::Int64Builder builder; // Make place for 8 values in total builder.Reserve(8); // Bulk append the given values (with a null in 4th place as indicated by the // validity vector) std::vector<bool> validity = {true, true, true, false, true, true, true, true}; std::vector<int64_t> values = {1, 2, 3, 0, 5, 6, 7, 8}; builder.AppendValues(values, validity); auto maybe_array = builder.Finish();

若必须逐个追加,部分具体 builder 子类提供标记为 "Unsafe" 的方法,它们假设工作区已被正确预置容量,以省去容量检查为代价换取更高性能:

arrow::Int64Builder builder; // Make place for 8 values in total builder.Reserve(8); builder.UnsafeAppend(1); builder.UnsafeAppend(2); builder.UnsafeAppend(3); builder.UnsafeAppendNull(); builder.UnsafeAppend(5); builder.UnsafeAppend(6); builder.UnsafeAppend(7); builder.UnsafeAppend(8); auto maybe_array = builder.Finish();

源码中的 Resize 与 Reserve 语义差异

阅读 ArrayBuilder 基类 的注释可以精确区分两者的语义:

  • Resize(int64_t capacity)确保已分配足够内存容纳指定数量的总元素(含已追加的);capacity必须大于当前容量,不能缩小(源码中会返回Invalid状态)。对变长数据(如 binary)不保证覆盖因重新分配产生的额外开销;
  • Reserve(int64_t additional_capacity)增量追加空间。注意additional_capacity是相对于当前元素数量而非当前容量计算的,内部会调用BufferBuilder::GrowByFactor做超分配(overallocation),以最小化多次增量Reserve()的影响;
  • UnsafeAppend*系列属于基类 protected 的 "Unsafe operations (don't check capacity/don't resize)" 类别,例如UnsafeAppendToBitmap(bool is_valid)直接向 null bitmap builder 写入位,不做任何容量检查。

这一实现印证了文档的性能建议:预置容量 + 批量/Unsafe 追加可以完全避免逐次调用时的边界检查与扩容判断。

4. 尺寸限制与 ChunkedArray

尺寸限制

部分数组类型在结构上受限于 32 位尺寸:例如list 数组最多容纳 2^31 个元素,string 数组与 binary 数组至少受限于 2GB 的二进制数据量。其他数组类型在 C++ 实现中可容纳多达 2^63 个元素,但其他 Arrow 语言实现对这些类型同样可能存在 32 位尺寸限制。基于此,官方建议:对超大数据应按更合理的规模切分为 chunk

ChunkedArray:逻辑连续、物理分块

arrow::ChunkedArray(定义见 chunked_array.h)与数组一样是一个逻辑值序列;但与简单数组不同的是,它不要求整个序列在内存中物理连续。组成 chunked array 的各分块可以大小不一,但必须具有相同的数据类型。

Chunked array 通过聚合任意数量的 array 构建。以下示例用两个独立 chunk 构建与上文相同的逻辑值序列:

std::vector<std::shared_ptr<arrow::Array>> chunks; std::shared_ptr<arrow::Array> array; // Build first chunk arrow::Int64Builder builder; builder.Append(1); builder.Append(2); builder.Append(3); if (!builder.Finish(&array).ok()) { // ... do something on array building failure } chunks.push_back(std::move(array)); // Build second chunk builder.Reset(); builder.AppendNull(); builder.Append(5); builder.Append(6); builder.Append(7); builder.Append(8); if (!builder.Finish(&array).ok()) { // ... do something on array building failure } chunks.push_back(std::move(array)); auto chunked_array = std::make_shared<arrow::ChunkedArray>(std::move(chunks)); assert(chunked_array->num_chunks() == 2); // Logical length in number of values assert(chunked_array->length() == 8); assert(chunked_array->null_count() == 1);

注意第二个 chunk 的构建直接复用了同一个builder——调用builder.Reset()后重新追加。这与源码中Finish会重置 builder 的语义(builder_base.h 的Reset()Finish注释)完全一致。最终断言的三个值体现了关键区别:length()返回的是逻辑长度(8 个值),而num_chunks()是物理分块数(2),null_count()跨所有 chunk 汇总为 1。

5. Slicing:数组的零拷贝切片

与物理内存 buffer 类似,数组和 chunked 数组同样支持零拷贝切片:得到仅引用数据某个逻辑子序列的数组或 chunked 数组,而不复制底层 buffer。实现方式是分别调用arrow::Array::Slicearrow::ChunkedArray::Slice方法。

这一机制在读取器(如 IPC/CSV reader)中极为常见——底层共享同一份 buffer,通过 offset + length 表达不同的逻辑视图,从而让分页读取、内存映射等场景零成本。

6. FromJSONString 辅助函数

仓库为示例、测试或快速原型提供了一组辅助函数,可以从 JSON 文本简洁地创建ArrayScalar这些辅助函数明确不应用于性能敏感场景——大多数用户应使用 JSON 文档 中描述的 API,它提供了从行分隔 JSON 文件高性能创建arrow::Tablearrow::RecordBatch的方式。

仓库中的可运行示例 from_json_string_example.cc 展示了ArrayFromJSONStringChunkedArrayFromJSONStringDictArrayFromJSONString三类辅助函数的用法(以下即该示例RunExample()的完整代码):

// Simple types ARROW_ASSIGN_OR_RAISE(auto int32_array, ArrayFromJSONString(arrow::int32(), "[1, 2, 3]")); ARROW_ASSIGN_OR_RAISE(auto float64_array, ArrayFromJSONString(arrow::float64(), "[4.0, 5.0, 6.0]")); ARROW_ASSIGN_OR_RAISE(auto bool_array, ArrayFromJSONString(arrow::boolean(), "[true, false, true]")); ARROW_ASSIGN_OR_RAISE( auto string_array, ArrayFromJSONString(arrow::utf8(), R"(["Hello", "World", null])")); // Timestamps can be created from string representations ARROW_ASSIGN_OR_RAISE( auto ts_array, ArrayFromJSONString(timestamp(arrow::TimeUnit::SECOND), R"(["1970-01-01", "2000-02-29","3989-07-14","1900-02-28"])")); // List, Map, Struct ARROW_ASSIGN_OR_RAISE( auto list_array, ArrayFromJSONString(list(arrow::int64()), "[[null], [], null, [4, 5, 6, 7, 8], [2, 3]]")); ARROW_ASSIGN_OR_RAISE( auto map_array, ArrayFromJSONString(map(arrow::utf8(), arrow::int32()), R"([[["joe", 0], ["mark", null]], null, [["cap", 8]], []])")); ARROW_ASSIGN_OR_RAISE( auto struct_array, ArrayFromJSONString( arrow::struct_({field("one", arrow::int32()), field("two", arrow::int32())}), "[[11, 22], null, [null, 33]]")); // ChunkedArrayFromJSONString ARROW_ASSIGN_OR_RAISE( auto chunked_array, ChunkedArrayFromJSONString(arrow::int32(), {"[5, 10]", "[null]", "[16]"})); // DictArrayFromJSONString ARROW_ASSIGN_OR_RAISE( auto dict_array, DictArrayFromJSONString(dictionary(arrow::int32(), arrow::utf8()), "[0, 1, 0, 2, 0, 3]", R"(["k1", "k2", "k3", "k4"])"));

从示例可以归纳这些辅助函数支持的类型覆盖面:

  • 简单类型:整型、浮点、布尔、字符串,JSON 中null直接映射为 null 值;
  • 时间戳:可从 ISO 风格的字符串表示创建(示例用TimeUnit::SECOND);
  • 嵌套类型:list、map、struct,JSON 的嵌套结构直接对应 Arrow 的嵌套布局;
  • ChunkedArrayChunkedArrayFromJSONString接收一个字符串序列,每个 JSON 字符串成为一个独立 chunk;
  • 字典数组DictArrayFromJSONString的第一个 JSON 字符串是 indices(如"[0, 1, 0, 2, 0, 3]"),第二个是 dictionary values(如["k1", "k2", "k3", "k4"]),组合出dictionary(int32, utf8)数组。

这些函数的声明位于 cpp/src/arrow/json/from_string.h,实现见 from_string.cc,对应测试为 from_string_test.cc。完整的辅助函数清单可参考 C++ API 参考中的 Array API(docs/source/cpp/api/array.rst)。

7. 实践要点小结

  1. 构建选路:内存布局已就绪时用Buffer+ArrayData直接包装;增量构建用ArrayBuilder子类;性能不敏感的示例/测试直接用*FromJSONString
  2. 性能路径:已知规模先Reserve(增量)或Resize(总量),批量用AppendValues,逐值场景用UnsafeAppend系列并自行保证容量;
  3. 容量语义Reserve(n)是"再容纳 n 个",Resize(n)是"总容量到 n",两者不可混用;
  4. 逻辑与物理分离ChunkedArraylength()是跨 chunk 的逻辑长度;超大数据应分块以规避 list/string/binary 等类型在部分实现中的 32 位尺寸限制;
  5. 零拷贝原则Slice不复制 buffer,适合做只读子序列视图;
  6. 不可变约定Array一经构建不可变,任何"修改"都意味着新对象,Finish后 builder 会被重置(DictionaryBuilder除外)可复用。

【免费下载链接】arrowApache Arrow is the universal columnar format and multi-language toolbox for fast data interchange and in-memory analytics项目地址: https://gitcode.com/GitHub_Trending/arrow3/arrow

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询