Skip to content

Commit c7e9284

Browse files
authored
refactor: use raw view access in do_append_val_inner and consolidate duplicated logic (#22907)
## Which issue does this PR close? Follow-up to #21794, addressing review feedback from @alamb and @Dandandan. ## Rationale for this change In the review of #21794, several optimizations were suggested: - **@alamb** ([comment](#21794 (comment))): "as a possible future optimization, we could use `get_unchecked` here if it makes any difference" — referring to the slow-path `arr.views()[row]` access. - **@alamb** ([comment](#21794 (comment))): "from here on down I think this is basically the same as append_val_inner -- if there are any differences perhaps we can fold it into append_val_inner and avoid the copy" - **@Dandandan** ([comment](#21794 (comment))): "In principle we can make this faster as well - `extend` + reuse input view (instead of make_view) + avoid `array.value(row)`" ## What changes are included in this PR? **1. Refactored `do_append_val_inner` to use raw view access** Replaced `array.value(row)` + `make_view()` with raw view access via `get_unchecked(row)`: - **Inline (len <= 12):** push the u128 view as-is — no decode/re-encode round-trip - **Non-inline (len > 12):** parse via `ByteView::from(view)`, copy buffer data, reuse source prefix directly (avoids re-reading first 4 bytes) **2. Simplified the vectorized slow path** Replaced the duplicated 28-line loop body in `vectorized_append_inner` with `try_reserve` + a loop calling `do_append_val_inner`, eliminating code duplication. **3. Removed unused `make_view` import** ### Safety notes - **`get_unchecked` usage**: Consistent with `do_equal_to_inner` (same file) and `PrimitiveGroupValueBuilder` in `primitive.rs`, both of which use the same pattern. All callers derive row indices from enumeration over the input array length, guaranteeing validity. - **Buffer access safety**: When `data_buffers()` is empty, all views must have len <= 12 (Arrow invariant), so the non-inline branch is never entered. ## Are these changes tested? Covered by 6 existing unit tests in the `bytes_view` module plus 3 integration tests in the `multi_group_by` module. All 111 tests in the aggregates suite pass. ## Are there any user-facing changes? No. This is an internal refactor with no API changes.
1 parent 408dad3 commit c7e9284

1 file changed

Lines changed: 29 additions & 48 deletions

File tree

  • datafusion/physical-plan/src/aggregates/group_values/multi_group_by

datafusion/physical-plan/src/aggregates/group_values/multi_group_by/bytes_view.rs

Lines changed: 29 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ use crate::aggregates::group_values::multi_group_by::{
2121
use crate::aggregates::group_values::null_builder::MaybeNullBufferBuilder;
2222
use arrow::array::{
2323
Array, ArrayRef, AsArray, BooleanBufferBuilder, ByteView, GenericByteViewArray,
24-
make_view,
2524
};
2625
use arrow::buffer::{Buffer, ScalarBuffer};
2726
use arrow::datatypes::ByteViewType;
@@ -176,43 +175,17 @@ impl<B: ByteViewType> ByteViewGroupValueBuilder<B> {
176175
// copy them directly instead of going through value() → make_view().
177176
self.views.extend(rows.iter().map(|&row| arr.views()[row]));
178177
} else {
179-
// Slow path: some strings are non-inline (>12 bytes).
180-
// Read views directly to avoid array.value(row) overhead and
181-
// reuse the source view's prefix instead of recomputing it via make_view.
178+
// Slow path: some strings may be non-inline (>12 bytes).
179+
// Pre-reserve and delegate to do_append_val_inner which
180+
// reads raw views directly and reuses source prefixes.
182181
self.views.try_reserve(rows.len()).map_err(|e| {
183182
datafusion_common::exec_datafusion_err!(
184183
"failed to reserve {0} views: {e}",
185184
rows.len()
186185
)
187186
})?;
188187
for &row in rows {
189-
let view = arr.views()[row];
190-
let len = view as u32;
191-
if len <= 12 {
192-
// This row happens to be inline; copy view directly.
193-
self.views.push(view);
194-
} else {
195-
let src = ByteView::from(view);
196-
// ensure_in_progress_big_enough must be called before computing
197-
// new_buffer_index / new_offset — it may flush in_progress to completed.
198-
self.ensure_in_progress_big_enough(len as usize);
199-
let new_buffer_index = self.completed.len() as u32;
200-
let new_offset = self.in_progress.len() as u32;
201-
let src_buf = &arr.data_buffers()[src.buffer_index as usize];
202-
self.in_progress.extend_from_slice(
203-
&src_buf[src.offset as usize
204-
..(src.offset + src.length) as usize],
205-
);
206-
// Reuse prefix from the source view — avoids re-reading first 4 bytes.
207-
let new_view = ByteView {
208-
length: src.length,
209-
prefix: src.prefix,
210-
buffer_index: new_buffer_index,
211-
offset: new_offset,
212-
}
213-
.as_u128();
214-
self.views.push(new_view);
215-
}
188+
self.do_append_val_inner(arr, row);
216189
}
217190
}
218191
}
@@ -230,25 +203,33 @@ impl<B: ByteViewType> ByteViewGroupValueBuilder<B> {
230203
where
231204
B: ByteViewType,
232205
{
233-
let value: &[u8] = array.value(row).as_ref();
206+
// SAFETY: the caller ensures `row` is valid
207+
let view = unsafe { *array.views().get_unchecked(row) };
208+
let len = view as u32;
234209

235-
let value_len = value.len();
236-
let view = if value_len <= 12 {
237-
make_view(value, 0, 0)
210+
if len <= 12 {
211+
// Inline value: the view is already self-contained, push as-is.
212+
self.views.push(view);
238213
} else {
239-
// Ensure big enough block to hold the value firstly
240-
self.ensure_in_progress_big_enough(value_len);
241-
242-
// Append value
243-
let buffer_index = self.completed.len();
244-
let offset = self.in_progress.len();
245-
self.in_progress.extend_from_slice(value);
246-
247-
make_view(value, buffer_index as u32, offset as u32)
248-
};
249-
250-
// Append view
251-
self.views.push(view);
214+
// Non-inline value: copy the buffer data and construct a new view
215+
// that points into our own buffers, reusing the source prefix.
216+
let src = ByteView::from(view);
217+
self.ensure_in_progress_big_enough(len as usize);
218+
let new_buffer_index = self.completed.len() as u32;
219+
let new_offset = self.in_progress.len() as u32;
220+
let src_buf = &array.data_buffers()[src.buffer_index as usize];
221+
self.in_progress.extend_from_slice(
222+
&src_buf[src.offset as usize..(src.offset + src.length) as usize],
223+
);
224+
let new_view = ByteView {
225+
length: src.length,
226+
prefix: src.prefix,
227+
buffer_index: new_buffer_index,
228+
offset: new_offset,
229+
}
230+
.as_u128();
231+
self.views.push(new_view);
232+
}
252233
}
253234

254235
fn ensure_in_progress_big_enough(&mut self, value_len: usize) {

0 commit comments

Comments
 (0)