perf: avoid cloning ByteView buffer lists in take and filter - #10708
perf: avoid cloning ByteView buffer lists in take and filter#10708YimingQiao wants to merge 3 commits into
Conversation
c884697 to
46c989e
Compare
There was a problem hiding this comment.
Thank you @YimingQiao and @Jefffrey -- this one looks good to me though I also have another potential idea (that would be more invasive)
| &self.buffers | ||
| } | ||
|
|
||
| /// Returns a cloned `Arc` of the buffers storing non-inline string or binary data. |
There was a problem hiding this comment.
I wonder if we should just change the other APIs be consistent -- like make data_buffers return &Arc<[Buffer]> and change new_unchecked to take Arc<[Buffer]> directly (rather than Into<Arc<[Buffer]>> )
That way we could find more places that unecessairly copy these buffers 🤔
There was a problem hiding this comment.
That worked nicely. I changed data_buffers() to return &Arc<[Buffer]> and made new_unchecked take Arc<[Buffer]> directly. Letting the compiler drive the update found three more .to_vec() call sites in the FFI, IPC, and Parquet tests; those now use Arc::clone, while callers that genuinely construct new buffer lists convert them explicitly. The workspace checks, relevant tests, clippy, and rustdoc all pass locally. This changes two public signatures, and I do not have permission to add labels here, so could you add api-change if appropriate?
alamb
left a comment
There was a problem hiding this comment.
Thank you @YimingQiao
@Jefffrey would you like to review this PR again before merging?
| /// [`data_buffers`]: Self::data_buffers | ||
| /// The returned `Arc` can be cloned to share the buffers with another array without | ||
| /// allocating a new collection or cloning the individual buffers. To consume this | ||
| /// array and take ownership of its buffers, use [`Self::into_parts`]. |
| fn test_utf8_view_ffi_from_dangling_pointer() { | ||
| let empty = GenericByteViewBuilder::<StringViewType>::new().finish(); | ||
| let buffers = empty.data_buffers().to_vec(); | ||
| let buffers = Arc::clone(empty.data_buffers()); |
There was a problem hiding this comment.
it is only a test, but this is a nice improvement
Which issue does this PR close?
Rationale for this change
This change follows directly from the existing ByteView design history:
StringViewArray::slice()andBinaryViewArray::slice()are slow (they allocate) #6408 traced slow ByteView slicing to allocating and cloning the backing-buffer list. The discussion specifically identifiedtakeas another operation that would benefit from sharing this list.Arc<[Buffer]>instead of rawVec<Buffer>inGenericByteViewArrayfor fasterslice#6427 distinguished two related but separate concerns: very large buffer lists may indicate missing GC or deduplication, but avoiding allocation when an operation retains the complete list is independently worthwhile.GenericByteViewArrayto store the collection asArc<[Buffer]>, making it possible to share the complete list without allocation.The downstream impact is concrete. apache/datafusion#16206 describes hash joins where concatenating build-side batches produces ByteView payload columns with many backing buffers, and constructing join output repeatedly calls
take. In that pattern, cloning and later dropping every buffer handle can become a significant part of execution time. Arrow issue #10692 provides a compact multi-stagetakeandBatchCoalescerreproduction of the same ownership-metadata amplification.take_byte_viewandfilter_byte_viewdo not yet use this shared representation. They rebuild the collection withdata_buffers().to_vec(), allocating a new collection and cloning everyBuffer. This makes the ownership bookkeeping for selection O(number of backing buffers), despite retaining exactly the same complete buffer list.Selection already copies the chosen views while leaving their payloads zero-copy. The cost of retaining the unchanged backing-buffer collection should therefore not grow with the number of entries in that collection. This PR completes that narrow part of the earlier design while leaving buffer canonicalization as a separate problem.
What changes are included in this PR?
GenericByteViewArray::data_buffers()to return&Arc<[Buffer]>, allowing callers to inspect the buffers as before or clone the collection'sArcin O(1).GenericByteViewArray::new_uncheckedto acceptArc<[Buffer]>directly, making shared versus newly constructed buffer-list ownership explicit at each call site.Arc::clonein the ByteViewtakeandfilterkernels instead of rebuilding the collection.data_buffers().to_vec()call sites found by the stricter constructor signature with sharedArcclones.This PR only removes repeated ownership-metadata cloning. It retains the same complete collection of backing buffers as before. It does not prune or deduplicate buffer entries, remap views, run GC, or copy string/binary payloads. The broader buffer-fragmentation problem described in #10692 remains separate.
Are these changes tested?
A temporary local Criterion benchmark was used to validate the asymptotic behavior. It takes 8,192 views distributed across a varying number of buffer entries. The entries share one immutable payload allocation, isolating collection-ownership cost from payload size.
mainand this PR were built in separate Cargo target directories on an Intel Xeon Platinum 8474C:mainExisting normal-size benchmarks did not regress:
maintake stringview 512take stringview 1024filter context mixed string view (kept 1/2)Are there any user-facing changes?
There are two public signature changes.
data_buffers()now returns&Arc<[Buffer]>instead of&[Buffer]; slice methods remain available through deref, while directforiteration can use.iter().new_uncheckednow requiresArc<[Buffer]>; callers constructing aVec<Buffer>can convert it with.into(). ByteView selection results now share the immutable backing-buffer collection instead of allocating an equivalent collection. Logical values, null handling, buffer indexes, payload lifetimes, and GC behavior are unchanged.AI assistance
I used OpenAI Codex to help inspect the relevant implementation history, draft the patch and tests, and prepare the benchmark harness and PR text. I reviewed the implementation and benchmark methodology and ran the checks above locally.