You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Excessive Arc-clone in HashJoinStream with StringView on build-side #16206
An unfortunate pattern in the hash join implementation leads to excessive Arc-cloning: Assume the build-side carries a string-view column as a payload. Let N be the number of batches seen on the build side
In the build phase, datafusion concatenates the batches on the build side. The string-view column now holds references to at least N data buffers in a vec;
When constructing the output batch, the take implementation for string-views clones the data buffer vector of the concatenated build-side column - thus incrementing the references on all N data buffers.
To Reproduce
I noticed this issue when executing and profiling tpch query 18 - roughly 3% of the runtime is spent cloning these Arcs.
Yeah - one of the options to optimize join is by avoiding the take + concat (which happens when generating fewer rows per input batch in join, so I think that could use a similar strategy).
Ideally, we also would avoid concat_batches in the start (and rather maintain aVec<RecordBatch>).
This was changed in the early days (by me) as it used to be slow.
But now that the hashmap is basically only comparing hash values during probe, I think we can try and avoid the concat_batches (and store a u32 / u32 of batch_id + index instead of a u64 index).
I've re-encountered this issue and it (obviously, duh) gets amplified with larger scale facctors (>100). For example, the top 3 items by cycles when running tpch query 18 @ sf 300 are:
That is close to 50% of cycles spent due to this issue. Because the batches that get produced by the hash join have a lot of data buffers, the CoalesceBatchesExec gets more expensive too: Inside the operator, we iterate over the data buffers to determine whether to perform a garbage collection.
I believe a reasonable fix for this issue is to perform a garbage collection on the build side after the concatenation. The build side would then only hold a single data buffer per string-view / byte-view column.
I think the issue on the take+concat pattern is only tangentially related to this issue. Ultimately, you would need a version of the take operation, that does not simply clone the vector of data buffers, but rather eagerly allocates a new data buffer to hold the string view data. This modification could be applied to the current compute::take kernel too.
The interleave kernel already does something like this: It only takes references to the data buffers that are actually referenced in the new array - that is why it would help somewhat alleviate this issue. If the build side is really large, this could still mean allocating a vector of 1000+ buffer references. The subsequent gc (in CoalesceBatchesExec) drops the vector and allocate a new data buffer.
Instead, I think there are 2 direct solutions for this:
Extend the take (or interleave) kernels to eagerly allocate new data buffers for string views (i.e. add a compute::TakeOption - unfortunately also a breaking change).
Switch from Vec<Buffer> to Arc<[Buffer]> - also a breaking change in the arrow crate. This would not require writing new kernel-style code - the take/interleave operations would remain unchanged.
My PR on executing a GC on the hash join build-side is unfortunately not a good solution: This GC operation is expensive, and whether it pays off depends on the number of probe side batches - which is not known in advance. The benchmark shows that it significantly slows down smaller joins. It would also not prevent the CoalesceBatchesExec after the HJ from issuing another GC.
For now, the best way to fix this issue is to work on getting apache/arrow-rs#6427 merged. As far as I can tell, it is already fully implemented and got stuck due to lack of time of the author to add the ViewBuffers abstraction.
Lastly, I believe that using interleave, or a good solution to the take+concat pattern would be very helpful in making the HJ more efficient in general. Most importantly, it could allow avoiding the concat on the build-side batches, and remove the CoalesceBatchesExec after the join.
Extend the take (or interleave) kernels to eagerly allocate new data buffers for string views (i.e. add a compute::TakeOption - unfortunately also a breaking change).
BTW this eagerly allocating buffers / copying string views when needed is what the (in progress) coalesce kernel does:
Activity
jonathanc-n commentedon May 29, 2025
take
alamb commentedon May 30, 2025
I have noticed something similar -- that the pattern of "take / concat" results in potentially non trival overhead.
I have been recently working on some related code (the same basic pattern happens for
filter) Related:Dandandan commentedon May 30, 2025
Yeah - one of the options to optimize join is by avoiding the
take+concat(which happens when generating fewer rows per input batch in join, so I think that could use a similar strategy).Ideally, we also would avoid
concat_batchesin the start (and rather maintain aVec<RecordBatch>).This was changed in the early days (by me) as it used to be slow.
But now that the hashmap is basically only comparing hash values during probe, I think we can try and avoid the
concat_batches(and store a u32 / u32 of batch_id + index instead of a u64 index).Dandandan commentedon May 30, 2025
This might help here as well I think (to avoid the excessive cloning)?
apache/arrow-rs#6427
jonathanc-n commentedon May 30, 2025
I'll try to give that a try, thanks @Dandandan
Dandandan commentedon May 31, 2025
So if we're to change
RecordBatchtoVec<RecordBatch>in the join code, I think this is what's roughly needed:batchtoVec<RecordBatch>inJoinLeftDataJoinLeftDatawith aVec<(usize, usize>)which has the batch_id and row_id for each indexVec<(usize, usize>)to methods based on the indices from the build side and theVec<(usize, usize>)structureinterleaverather thantakectsk commentedon Jun 19, 2025
I've re-encountered this issue and it (obviously, duh) gets amplified with larger scale facctors (>100). For example, the top 3 items by cycles when running tpch query 18 @ sf 300 are:
That is close to 50% of cycles spent due to this issue. Because the batches that get produced by the hash join have a lot of data buffers, the CoalesceBatchesExec gets more expensive too: Inside the operator, we iterate over the data buffers to determine whether to perform a garbage collection.
I believe a reasonable fix for this issue is to perform a garbage collection on the build side after the concatenation. The build side would then only hold a single data buffer per string-view / byte-view column.
Dandandan commentedon Jun 19, 2025
Thanks for sharing the insight @ctsk
I think there is some overlap here to the work @alamb to improve (and maybe remove over time)
CoalesceBatchesExecand gc of views.alamb commentedon Jun 20, 2025
The relevant arrow ticket is
@Dandandan also made a PR that improves some view code here (but I don't think it affects take_view) here:
interleave_views(4-7x faster) arrow-rs#7695ctsk commentedon Jun 22, 2025
I think the issue on the take+concat pattern is only tangentially related to this issue. Ultimately, you would need a version of the take operation, that does not simply clone the vector of data buffers, but rather eagerly allocates a new data buffer to hold the string view data. This modification could be applied to the current compute::take kernel too.
The interleave kernel already does something like this: It only takes references to the data buffers that are actually referenced in the new array - that is why it would help somewhat alleviate this issue. If the build side is really large, this could still mean allocating a vector of 1000+ buffer references. The subsequent gc (in CoalesceBatchesExec) drops the vector and allocate a new data buffer.
Instead, I think there are 2 direct solutions for this:
compute::TakeOption- unfortunately also a breaking change).Vec<Buffer>toArc<[Buffer]>- also a breaking change in the arrow crate. This would not require writing new kernel-style code - the take/interleave operations would remain unchanged.My PR on executing a GC on the hash join build-side is unfortunately not a good solution: This GC operation is expensive, and whether it pays off depends on the number of probe side batches - which is not known in advance. The benchmark shows that it significantly slows down smaller joins. It would also not prevent the CoalesceBatchesExec after the HJ from issuing another GC.
For now, the best way to fix this issue is to work on getting apache/arrow-rs#6427 merged. As far as I can tell, it is already fully implemented and got stuck due to lack of time of the author to add the
ViewBuffersabstraction.Lastly, I believe that using interleave, or a good solution to the take+concat pattern would be very helpful in making the HJ more efficient in general. Most importantly, it could allow avoiding the concat on the build-side batches, and remove the CoalesceBatchesExec after the join.
alamb commentedon Jun 25, 2025
BTW this eagerly allocating buffers / copying string views when needed is what the (in progress)
coalescekernel does:Here is the logic that does it for StringView
I hope to migrate DataFusion to use the BatchCoalescer soon.
There is a
BatchCoalescer::push_batch_with_filter(that does the equivalent offilter+concat) that I am actively working to improveI imagine implementing something like
BatchCoalescer::push_batch_with_indicesthat does the equivalent oftake+concatExpose `&Arc[Buffer]` in `GenericByteViewArray::data_buffers` for che…
Expose `&Arc[Buffer]` in `GenericByteViewArray::data_buffers` for che…
Expose `&Arc[Buffer]` in `GenericByteViewArray::data_buffers` for che…
Expose `&Arc[Buffer]` in `GenericByteViewArray::data_buffers` for che…
Expose `&Arc[Buffer]` in `GenericByteViewArray::data_buffers` for che…
Expose `&Arc[Buffer]` in `GenericByteViewArray::data_buffers` for che…
adriangb commentedon Sep 24, 2026
@ctsk can you check if this still reproduces? Datafusion now runs on arrow-rs 60.X which includes apache/arrow-rs#10708
Expose `&Arc[Buffer]` in `GenericByteViewArray::data_buffers` for che…