ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Apache Arrow C++ 数组(Array)开发指南:核心数据结构、ArrayBuilder 构建、ChunkedArray 与零拷贝切片

Apache Arrow C++ 数组(Array)开发指南:核心数据结构、ArrayBuilder 构建、ChunkedArray 与零拷贝切片 Apache Arrow C 数组Array开发指南核心数据结构、ArrayBuilder 构建、ChunkedArray 与零拷贝切片【免费下载链接】arrowApache Arrow is a multi-language toolbox for accelerated data interchange and in-memory processing项目地址: https://gitcode.com/gh_mirrors/arrow12/arrowApache Arrow C 的核心数据容器是arrow::Array——一个长度已知、所有值类型相同的内存序列。本文以 docs/source/cpp/arrays.rst 为骨架系统讲解 Arrow C 中数组的内存布局值缓冲区 可选的 null 位图、三种构建策略包装既有内存、ArrayBuilder增量构建、批量/Unsafe 高性能追加、ChunkedArray分块逻辑与Slice零拷贝切片并结合 cpp/src/arrow/array/ 目录下的源码实现帮助读者写出正确、高效且可复用的 Arrow 数据处理代码。核心概念arrow::Array 与底层 BufferArrow 中位于核心位置的类是arrow::Array其定义位于 cpp/src/arrow/array/array_base.h。一个数组表示一个已知长度的、所有值都具有相同数据类型的序列。在内部这些值由一个或多个arrow::Buffer表示缓冲区的数量和语义取决于数组的数据类型这部分在 Arrow 数据布局规范format/目录下的规范文档中有详细定义。这些缓冲区包括两部分值数据本身value data可选的位图缓冲区bitmap buffer用于标识数组中的哪些条目是 null 值。当数组已知没有任何 null 值时位图缓冲区可以完全省略。这一点在源码中得到印证array_base.h 中null_bitmap()的注释明确指出对于null_count 0或 null 类型该缓冲区为 nullIsValid()的实现array_base.h也会在null_bitmap_data_为空指针时走无位图分支。针对每一种数据类型arrow::Array都有对应的具体子类方便你直接访问数组中的单个值例如arrow::Int64Array、arrow::StringArray、arrow::ListArray等。底层数据结构 ArrayData数组的元数据与缓冲区集合存放在arrow::ArrayData结构体中cpp/src/arrow/array/data.h它持有type、length、null_count、offset、buffers以及嵌套类型的child_data。其中buffers[0]约定为有效性null位图。值得注意的常量是kUnknownNullCount -1data.h当 null 数未知时典型场景是切片之后null_count先置为 -1首次调用null_count()时才计算并缓存。构建一个数组三种可用策略由于 Arrow 对象是**不可变immutable**的它们无法像std::vector那样直接原地填充。构建数组主要有三种策略策略一包装已存在的内存如果数据已经以正确的布局存在于内存中可以先把这块内存包装进arrow::Buffer实例再构造一个描述该数组的arrow::ArrayData。这种方式的典型价值在于零拷贝复用已有内存例如从文件、网络或共享内存中读出的连续区域具体的内存管理细节可参考 Arrow C 的内存管理相关文档docs/source/cpp/目录下的内存管理章节。策略二使用 ArrayBuilder 增量构建否则推荐使用arrow::ArrayBuilder基类及其具体子类它们帮你逐步累积数组数据而无需自行处理 Arrow 格式的细节例如位图的字节序与偏移计算。ArrayBuilder的基类实现在 cpp/src/arrow/array/builder_base.h 中它负责维护长度、null 计数、容量并内部使用一个null_bitmap_builder_增量构建 null 位图。策略三直接使用具体子类构造函数对于已构造好的ArrayData也可以通过std::make_sharedInt64Array(array_data)这类方式直接构造具体类型的数组ArrayData到强类型Array的转换在源码中随处可见这通常与策略一配合使用。实战示例用 Int64Builder 构建含 null 的数组要构建一个Int64类型的 Arrow 数组可以使用arrow::Int64Builder类。下面这个示例构建一个 1 到 8 的数组其中第 4 个元素本应存放 4 的位置被置为 nullarrow::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()) { // ... 数组构建失败时做相应处理 } std::shared_ptrarrow::Array array *maybe_array;生成的数组如果需要访问其值可以强转成具体的arrow::Int64Array子类由两个arrow::Buffer组成第一个缓冲区是 null 位图。这里的位图只有一个字节位为1|1|1|1|0|1|1|1。由于 Arrow 使用最低有效位LSB编号least-significant bit numbering这表示数组中第 4 个条目是 null。第二个缓冲区是一个int64_t数组包含上述所有值。由于第 4 个条目是 null缓冲区中该位置的值是未定义的undefined访问前必须先用有效性位图判断。访问具体数组的内容构建之后可以通过如下方式访问数组的实际数据// 将 Array 强转为其实际类型以访问数据 auto int64_array std::static_pointer_castarrow::Int64Array(array); // 获取 null 位图的指针 const uint8_t* null_bitmap int64_array-null_bitmap_data(); // 获取实际数据的指针 const int64_t* data int64_array-raw_values(); // 或者给定数组索引直接查询其 null 位与值 int64_t index 2; if (!int64_array-IsNull(index)) { int64_t value int64_array-Value(index); }null_bitmap_data()与raw_values()返回的是原始指针适合高性能批量处理IsNull(index)与Value(index)是面向单值访问的便利接口IsNull在 array_base.h 中定义为!IsValid(i)实现为内联位测试见 array_base.h。关于类型的说明arrow::Int64Array相应地arrow::Int64Builder只是arrow::NumericArrayInt64Type相应地arrow::NumericBuilderInt64Type的便捷typedef。对应的模板定义位于 cpp/src/arrow/array/array_primitive.h 与 cpp/src/arrow/array/builder_primitive.h。因此本文的所有用法同样适用于Int32Array、DoubleArray、UInt64Array等其他数值类型。性能建议批量追加、预分配与 Unsafe 方法虽然可以像上面那样逐个值构建数组但若要达到最高性能建议使用具体ArrayBuilder子类中的批量追加方法通常命名为AppendValues。同时如果你预先知道元素个数强烈建议先调用Resize或Reserve预分配工作区。二者的区别在 builder_base.h 的源码注释中写得很清楚Resize(capacity)确保分配的内存能容纳总共指定数量的元素包括已追加的不考虑变长数据如 binary 值因追加导致的再分配Reserve(additional_capacity)确保能再追加指定数量元素而无需进一步重新分配内部会通过BufferBuilder::GrowByFactor进行带增长因子的整体扩张overallocation以减少频繁小步 Reserve 的代价。下面是利用这些 API 重写上述示例的方式arrow::Int64Builder builder; // 为总共 8 个值预留空间 builder.Reserve(8); // 批量追加给定值validity 向量指示第 4 个位置为 null std::vectorbool validity {true, true, true, false, true, true, true, true}; std::vectorint64_t values {1, 2, 3, 0, 5, 6, 7, 8}; builder.AppendValues(values, validity); auto maybe_array builder.Finish();如果确实必须逐值追加一些具体 builder 子类还提供了名称中带 Unsafe 的方法它们假定工作区已被正确预分配从而换取更高的性能——但一旦预分配不足就是未定义行为使用前务必先Reserve或Resizearrow::Int64Builder builder; // 为总共 8 个值预留空间 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();从 builder_base.h 还可以看到AppendOrNull/UnsafeAppendOrNull这类便利方法它们接受std::optionalV有值时追加值、无值时追加 null非常适合从可选值流构建数组的场景。大小限制与分块建议部分数组类型在结构上被限制为 32 位大小list 数组最多可容纳 2^31 个元素源码 builder_base.h 中kListMaximumElements std::numeric_limitsint32_t::max() - 1印证了这一点string 数组与 binary 数组最多可容纳 2GB 的二进制数据。另一些数组类型在 C 实现中可以容纳多达 2^63 个元素但其他 Arrow 语言实现可能对这类数组同样存在 32 位大小限制。因此建议将海量数据切分成规模更合理的块chunk来处理——这正是下一节ChunkedArray存在的意义。ChunkedArray逻辑连续、物理分块的序列arrow::ChunkedArray与普通数组一样是一个逻辑上的值序列但与普通数组不同分块数组不要求整个序列在物理内存中连续。分块数组的各个组成部分chunk大小可以不同但数据类型必须一致。其类定义与完整注释见 cpp/src/arrow/chunked_array.h。从源码注释可以提炼出分块设计的两大动机chunked_array.h性能与内存优化数据分块在整个项目中被视为性能与内存优化的实现细节ChunkedArray允许把多个Array当作单一逻辑数组解释而无需昂贵的拼接concatenation步骤容量与并行产出的现实约束某些函数产出的数据可能超过单个Array的容量如BinaryArray/StringArray此时返回多个数组是唯一选择并行处理时也未必能甚至根本不可能如 binary/string 类型直接写入一块大的连续内存并产出非分块输出。分块数组通过聚合任意数量的数组来构造。下面构建一个与前面示例逻辑值相同、但分成两个 chunk 的分块数组std::vectorstd::shared_ptrarrow::Array chunks; std::shared_ptrarrow::Array array; // 构建第一个 chunk arrow::Int64Builder builder; builder.Append(1); builder.Append(2); builder.Append(3); if (!builder.Finish(array).ok()) { // ... 数组构建失败时做相应处理 } chunks.push_back(std::move(array)); // 构建第二个 chunk复用同一个 builder builder.Reset(); builder.AppendNull(); builder.Append(5); builder.Append(6); builder.Append(7); builder.Append(8); if (!builder.Finish(array).ok()) { // ... 数组构建失败时做相应处理 } chunks.push_back(std::move(array)); auto chunked_array std::make_sharedarrow::ChunkedArray(std::move(chunks)); assert(chunked_array-num_chunks() 2); // 逻辑长度值的个数 assert(chunked_array-length() 8); assert(chunked_array-null_count() 1);这里的关键 API 及其语义见 chunked_array.hlength()所有 chunk 的总长度构造时即计算null_count()所有 chunk 中 null 的总数num_chunks()chunk 的个数chunk(i)/chunks()访问第 i 个 chunk 或全部 chunk 的向量builder.Reset()复用同一个 builder 开始新一轮构建避免重复创建对象。ChunkedArray还支持Make(ArrayVector, type)带校验构造、MakeEmpty(type, pool)创建空分块数组、View视图转换以及Validate/ValidateFull校验等能力。另外需要留意的是chunked_array.hchunk 的布局/大小一般不作为 API 契约处理函数可能改变结果的 chunk 划分接受多个ChunkedArray输入的 API 也不应假设各输入 chunk 布局一致。零拷贝切片Slicing与物理内存缓冲区的切片类似你可以对数组和分块数组进行零拷贝切片zero-copy slice得到一个引用原数据某个逻辑子序列的新数组或分块数组。做法分别是调用arrow::Array::Slice和arrow::ChunkedArray::Slice方法。Array::Slice(offset, length)与Slice(offset)从 offset 切到末尾的声明在 array_base.h实现位于 array_base.cc它本质上是对底层ArrayData::Slice的封装其中SliceSafe变体会先做参数校验。零拷贝的底层原理体现在 cpp/src/arrow/array/data.cc 的ArrayData::Slice实现中它并不复制值缓冲区而是浅拷贝ArrayData结构将offset累加、length截断到实际剩余范围null 计数的处理很巧妙若原数组全部为 nullnull_count length切片后 null 数直接取切片长度若切片恰好覆盖整个原数组则沿用已知的 null 数否则置为kUnknownNullCount-1首次访问null_count()时再通过 data.cc 中的GetNullCount()按位统计并缓存。这意味着切片的开销是 O(1) 级别的元数据操作而不论切片长度多大。同样的语义也适用于ChunkedArray::Slicechunked_array.h它会在 chunk 边界上做裁剪必要时返回一个仅覆盖部分 chunk 的新分块数组。总结与进一步阅读围绕arrow::Array本文覆盖了从内存布局、增量/批量/Unsafe 构建、null 位图语义、大小限制、ChunkedArray分块到零拷贝切片的全链路知识。核心要点可以归纳为数组不可变要么包装既有内存BufferArrayData要么用ArrayBuilder系列增量构建null 通过位图表达LSB 编号无 null 时可省略位图未定 null 数用kUnknownNullCount惰性计算性能三件套AppendValues批量追加、Resize/Reserve预分配、确认容量后的Unsafe*方法大数组请分块受 32 位大小限制的 list/string/binary 类型尤其如此ChunkedArray让分块对上层逻辑透明切片零拷贝Slice只改元数据offset/length/null_count不复制数据。想继续深入可以阅读数组基类与访问器源码cpp/src/arrow/array/array_base.h、cpp/src/arrow/array/array_base.cc数组数据容器cpp/src/arrow/array/data.h、cpp/src/arrow/array/data.cc构建器体系cpp/src/arrow/array/builder_base.h、cpp/src/arrow/array/builder_primitive.h分块数组cpp/src/arrow/chunked_array.h、cpp/src/arrow/chunked_array.cc各类数组的单元测试例如 cpp/src/arrow/array/array_test.cc 与 cpp/src/arrow/array/builder_binary_test.cc以array_test.cc为起点可找到大量 Slice、null 位图与构建器的行为验证。另外Arrow 的二进制 IPC 格式与内存布局规范定义在仓库根目录的 format/ 目录如Schema.fbs、Message.fbs、File.fbs是理解数组缓冲区布局的权威参考。【免费下载链接】arrowApache Arrow is a multi-language toolbox for accelerated data interchange and in-memory processing项目地址: https://gitcode.com/gh_mirrors/arrow12/arrow创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表