数组#
另请参阅
Arrow 中的核心类型是类 arrow::Array
。数组表示一个已知长度的序列,所有值都具有相同的类型。在内部,这些值由一个或多个缓冲区表示,缓冲区的数量和含义取决于数组的数据类型,如 Arrow 数据布局规范 中所述。
这些缓冲区包含值数据本身,以及一个可选的位图缓冲区,指示数组中的哪些条目为 null 值。如果已知数组没有 null 值,则可以完全省略位图缓冲区。
对于每种数据类型,都有 arrow::Array
的具体子类,这些子类可以帮助您访问数组中的各个值。
构建数组#
可用策略#
由于 Arrow 对象是不可变的,因此它们不能像 std::vector
那样直接填充。相反,可以使用几种策略
如果数据已存在于内存中,并且具有正确的布局,您可以将这些内存包装在
arrow::Buffer
实例中,然后构造一个描述数组的arrow::ArrayData
;另请参阅
否则,
arrow::ArrayBuilder
基类及其具体子类可以帮助您增量构建数组数据,而无需自己处理 Arrow 格式的细节。
使用 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
。第一个缓冲区包含 null 位图,这里包含一个字节,位为 1|1|1|1|0|1|1|1
。由于我们使用的是 最低有效位(LSB)编号,这表示数组中的第四个条目为 null。第二个缓冲区只是一个包含所有上述值的 int64_t
数组。由于第四个条目为 null,因此缓冲区中该位置的值未定义。
以下是访问具体数组内容的方法
// 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::Int64Builder
)只是一个 typedef
,为了方便起见,提供了 arrow::NumericArray<Int64Type>
(分别为 arrow::NumericBuilder<Int64Type>
)。
性能#
虽然可以像上面的示例那样逐个构建数组值,但为了获得最佳性能,建议在具体的 arrow::ArrayBuilder
子类中使用批量追加方法(通常命名为 AppendValues
)。
如果您事先知道元素的数量,建议通过调用 Resize()
或 Reserve()
方法来预先调整工作区域的大小。
以下是如何改写上面的示例以利用这些 API
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();
如果您仍然必须逐个追加值,一些具体的构建器子类具有标记为“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();
大小限制和建议#
某些数组类型在结构上限制为 32 位大小。对于列表数组(最多可容纳 2^31 个元素)、字符串数组和二进制数组(最多可容纳 2GB 的二进制数据)来说,情况就是如此。至少,其他一些数组类型在 C++ 实现中最多可以容纳 2^63 个元素,但其他 Arrow 实现可能对这些数组类型也有 32 位大小限制。
出于这些原因,建议将庞大的数据分成更合理大小的子集。
分块数组#
一个 arrow::ChunkedArray
,就像一个数组,是一个值的逻辑序列;但与简单的数组不同,分块数组不需要整个序列在内存中物理上连续。此外,分块数组的组成部分不需要具有相同的大小,但它们必须都具有相同的数据类型。
分块数组是通过聚合任意数量的数组来构建的。在这里,我们将构建一个分块数组,其逻辑值与上面的示例相同,但在两个单独的块中
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);
切片#
与物理内存缓冲区一样,也可以对数组和分块数组进行零拷贝切片,以获得引用数据某些逻辑子序列的数组或分块数组。这是通过分别调用 arrow::Array::Slice()
和 arrow::ChunkedArray::Slice()
方法来完成的。