Repository navigation
GH-50994: [C++][Compute] Implement casting from ListView to List with zero-copy fast-path #50976
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
125ab1c
289afef
53085f0
beb87f7
ce16dcc
68def9d
5e4cf34
6bd6303
5d7c759
c929f7e
b312f28
d1cdcf2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,8 +26,10 @@ | |
| #include "arrow/array/builder_primitive.h" | ||
| #include "arrow/compute/api_scalar.h" | ||
| #include "arrow/compute/cast.h" | ||
| #include "arrow/compute/exec.h" | ||
| #include "arrow/compute/kernels/common_internal.h" | ||
| #include "arrow/compute/kernels/scalar_cast_internal.h" | ||
| #include "arrow/util/bit_run_reader.h" | ||
| #include "arrow/util/bitmap_ops.h" | ||
| #include "arrow/util/int_util.h" | ||
| #include "arrow/util/logging_internal.h" | ||
|
|
@@ -141,6 +143,136 @@ void AddListCast(CastFunction* func) { | |
| DCHECK_OK(func->AddKernel(SrcType::type_id, std::move(kernel))); | ||
| } | ||
|
|
||
| // (Large)ListView<T> -> (Large)List<U> | ||
| template <typename SrcType, typename DestType> | ||
| struct CastListViewToVarList { | ||
| using src_offset_type = typename SrcType::offset_type; | ||
| using dest_offset_type = typename DestType::offset_type; | ||
|
|
||
| static constexpr bool is_downcast = sizeof(src_offset_type) > sizeof(dest_offset_type); | ||
|
|
||
| static Status Exec(KernelContext* ctx, const ExecSpan& batch, ExecResult* out) { | ||
| const CastOptions& options = CastState::Get(ctx); | ||
| auto child_type = checked_cast<const DestType&>(*out->type()).value_type(); | ||
| const ArraySpan& in_array = batch[0].array; | ||
| ArrayData* out_array = out->array_data().get(); | ||
|
|
||
| DCHECK_NE(in_array.length, 0); | ||
|
|
||
| ARROW_ASSIGN_OR_RAISE(out_array->buffers[0], | ||
| GetOrCopyNullBitmapBuffer(in_array, ctx->memory_pool())); | ||
|
|
||
| std::shared_ptr<ArrayData> values = in_array.child_data[0].ToArrayData(); | ||
|
|
||
| const auto* offsets = in_array.GetValues<src_offset_type>(1); | ||
| const auto* sizes = in_array.GetValues<src_offset_type>(2); | ||
|
|
||
| // Allocate destination offsets buffer | ||
| ARROW_ASSIGN_OR_RAISE(out_array->buffers[1], ctx->Allocate(sizeof(dest_offset_type) * | ||
| (in_array.length + 1))); | ||
| auto* dest_offsets = out_array->GetMutableValues<dest_offset_type>(1); | ||
|
|
||
| // Single-pass: compute contiguous destination offsets and verify contiguity | ||
| src_offset_type start_offset = offsets[0]; | ||
| bool is_contiguous = true; | ||
|
|
||
| for (int64_t i = 0; i < in_array.length; ++i) { | ||
| dest_offsets[i] = static_cast<dest_offset_type>(offsets[i] - start_offset); | ||
| if (in_array.IsNull(i)) { | ||
| if (sizes[i] != 0) { | ||
| is_contiguous = false; | ||
| } | ||
| } else if (i < in_array.length - 1 && offsets[i] + sizes[i] != offsets[i + 1]) { | ||
| is_contiguous = false; | ||
| } | ||
| } | ||
| src_offset_type abs_end_offset = | ||
| offsets[in_array.length - 1] + sizes[in_array.length - 1]; | ||
| dest_offsets[in_array.length] = | ||
| static_cast<dest_offset_type>(abs_end_offset - start_offset); | ||
|
|
||
| if (is_contiguous) { | ||
| // Zero-copy fast-path: dest_offsets are already computed, slice child values | ||
| src_offset_type range = abs_end_offset - start_offset; | ||
| if constexpr (is_downcast) { | ||
| if (range > std::numeric_limits<dest_offset_type>::max()) { | ||
| return Status::Invalid("Array of type ", in_array.type->ToString(), | ||
| " too large to convert to ", | ||
| out_array->type->ToString()); | ||
| } | ||
| } | ||
|
|
||
| values = values->Slice(start_offset, range); | ||
| } else { | ||
| // Non-contiguous path: recompute offsets using SetBitRunReader, flatten values | ||
| src_offset_type current_offset = 0; | ||
| dest_offsets[0] = 0; | ||
| const uint8_t* validity = in_array.buffers[0].data; | ||
|
|
||
| if (validity == nullptr) { | ||
| for (int64_t i = 0; i < in_array.length; ++i) { | ||
| current_offset += sizes[i]; | ||
| dest_offsets[i + 1] = static_cast<dest_offset_type>(current_offset); | ||
| } | ||
| } else { | ||
| arrow::internal::SetBitRunReader reader(validity, in_array.offset, | ||
| in_array.length); | ||
| int64_t last_idx = 0; | ||
| while (true) { | ||
| const auto run = reader.NextRun(); | ||
| if (run.length == 0) { | ||
| break; | ||
| } | ||
| for (int64_t i = last_idx; i < run.position; ++i) { | ||
| dest_offsets[i + 1] = static_cast<dest_offset_type>(current_offset); | ||
| } | ||
| for (int64_t i = run.position; i < run.position + run.length; ++i) { | ||
| current_offset += sizes[i]; | ||
| dest_offsets[i + 1] = static_cast<dest_offset_type>(current_offset); | ||
| } | ||
| last_idx = run.position + run.length; | ||
| } | ||
| for (int64_t i = last_idx; i < in_array.length; ++i) { | ||
| dest_offsets[i + 1] = static_cast<dest_offset_type>(current_offset); | ||
| } | ||
| } | ||
|
Comment on lines
+208
to
+238
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you could simplify the implementation by having the same loop offsets for both branches. That loop would compute all destination offsets and compute whether the source entries are contiguous, all in one go. The contiguity information is mostly useful to know how to compute
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Jay846 You didn't address this comment, did you investigate this idea?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hi @pitrou — Yes, we investigated and implemented that idea in commit 6bd6303 by unifying dest_offsets[i + 1] outside the if/else. Following up on your SetBitRunReader suggestion, commit c929f7e then upgraded the non-contiguous path to use SetBitRunReader for word-at-a-time validity scanning (with a direct fast-path when validity == nullptr), which replaced the per-element IsNull(i) checks entirely!
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This part wasn't addressed:
i.e. try to fold the contiguity check (
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hi @pitrou, Thanks for clarifying! I've updated the kernel to fold the contiguity check directly into the primary destination offset calculation loop. In a single pass over the array, dest_offsets are populated for the zero-copy path while simultaneously validating contiguity. If contiguous, dest_offsets are already ready and child values are sliced with zero memory overhead. If non-contiguous, it gracefully falls back to the SetBitRunReader flattening path. The standalone IsContiguous() helper has been removed. |
||
|
|
||
| if constexpr (is_downcast) { | ||
| if (current_offset > std::numeric_limits<dest_offset_type>::max()) { | ||
| return Status::Invalid("Array of type ", in_array.type->ToString(), | ||
| " too large to convert to ", | ||
| out_array->type->ToString()); | ||
| } | ||
| } | ||
|
|
||
| // Use TypeTraits<SrcType>::ArrayType::Flatten to get values | ||
| using ArrayType = typename TypeTraits<SrcType>::ArrayType; | ||
| auto input_array = MakeArray(in_array.ToArrayData()); | ||
| const auto& list_view_array = checked_cast<const ArrayType&>(*input_array); | ||
| ARROW_ASSIGN_OR_RAISE(auto flattened, list_view_array.Flatten(ctx->memory_pool())); | ||
| values = flattened->data(); | ||
| } | ||
|
|
||
| // Cast values | ||
| ARROW_ASSIGN_OR_RAISE(Datum cast_values, | ||
| Cast(values, child_type, options, ctx->exec_context())); | ||
| DCHECK(cast_values.is_array()); | ||
| out_array->child_data.push_back(cast_values.array()); | ||
|
|
||
| return Status::OK(); | ||
| } | ||
| }; | ||
|
|
||
| template <typename SrcType, typename DestType> | ||
| void AddListViewCast(CastFunction* func) { | ||
| ScalarKernel kernel; | ||
| kernel.exec = CastListViewToVarList<SrcType, DestType>::Exec; | ||
| kernel.signature = | ||
| KernelSignature::Make({InputType(SrcType::type_id)}, kOutputTargetType); | ||
| kernel.null_handling = NullHandling::COMPUTED_NO_PREALLOCATE; | ||
| DCHECK_OK(func->AddKernel(SrcType::type_id, std::move(kernel))); | ||
| } | ||
|
|
||
| template <typename DestType> | ||
| struct CastFixedToVarList { | ||
| using dest_offset_type = typename DestType::offset_type; | ||
|
|
@@ -487,18 +619,18 @@ std::vector<std::shared_ptr<CastFunction>> GetNestedCasts() { | |
| auto cast_list = std::make_shared<CastFunction>("cast_list", Type::LIST); | ||
| AddCommonCasts(Type::LIST, kOutputTargetType, cast_list.get()); | ||
| AddListCast<ListType, ListType>(cast_list.get()); | ||
| AddListCast<ListViewType, ListType>(cast_list.get()); | ||
| AddListViewCast<ListViewType, ListType>(cast_list.get()); | ||
| AddListCast<LargeListType, ListType>(cast_list.get()); | ||
| AddListCast<LargeListViewType, ListType>(cast_list.get()); | ||
| AddListViewCast<LargeListViewType, ListType>(cast_list.get()); | ||
| AddTypeToTypeCast<CastFixedToVarList<ListType>, FixedSizeListType>(cast_list.get()); | ||
|
|
||
| auto cast_large_list = | ||
| std::make_shared<CastFunction>("cast_large_list", Type::LARGE_LIST); | ||
| AddCommonCasts(Type::LARGE_LIST, kOutputTargetType, cast_large_list.get()); | ||
| AddListCast<ListType, LargeListType>(cast_large_list.get()); | ||
| AddListCast<ListViewType, LargeListType>(cast_large_list.get()); | ||
| AddListViewCast<ListViewType, LargeListType>(cast_large_list.get()); | ||
| AddListCast<LargeListType, LargeListType>(cast_large_list.get()); | ||
| AddListCast<LargeListViewType, LargeListType>(cast_large_list.get()); | ||
| AddListViewCast<LargeListViewType, LargeListType>(cast_large_list.get()); | ||
| AddTypeToTypeCast<CastFixedToVarList<LargeListType>, FixedSizeListType>( | ||
| cast_large_list.get()); | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's add a comment summarizing this:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added // (Large)ListView -> (Large)List comment before the template