Skip to content

Commit 1d3e6b4

Browse files
authored
take() on VarBin returns VarBinView (#9229)
take() on VarBin returned a VarBin which was slow because referenced bytes were copied. However, FSST version requires this. Make take() return VarBinView but keep the old version for FSST. Add a take benchmark for VarBin Resolves: #4964 --------- Signed-off-by: Mikhail Kot <mikhail@spiraldb.com>
1 parent 6a8d248 commit 1d3e6b4

7 files changed

Lines changed: 200 additions & 76 deletions

File tree

encodings/fsst/src/compute/mod.rs

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,11 @@ use vortex_array::ArrayRef;
1111
use vortex_array::ArrayView;
1212
use vortex_array::ExecutionCtx;
1313
use vortex_array::IntoArray;
14-
use vortex_array::arrays::VarBin;
1514
use vortex_array::arrays::dict::TakeExecute;
15+
use vortex_array::arrays::varbin::take_varbin;
1616
use vortex_array::builtins::ArrayBuiltins;
1717
use vortex_array::scalar::Scalar;
18-
use vortex_error::VortexExpect;
1918
use vortex_error::VortexResult;
20-
use vortex_error::vortex_err;
2119

2220
use crate::FSST;
2321
use crate::FSSTArrayExt;
@@ -36,14 +34,7 @@ impl TakeExecute for FSST {
3634
.clone()
3735
.union_nullability(indices.dtype().nullability()),
3836
array.symbol_table(),
39-
{
40-
let codes = array.codes();
41-
let codes = codes.as_view();
42-
<VarBin as TakeExecute>::take(codes, indices, ctx)?
43-
.vortex_expect("VarBin take kernel always returns Some")
44-
}
45-
.try_downcast::<VarBin>()
46-
.map_err(|_| vortex_err!("take for codes must return varbin array"))?,
37+
take_varbin(array.codes().as_view(), indices, ctx)?,
4738
array
4839
.uncompressed_lengths()
4940
.take(indices.clone())?

vortex-array/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,10 @@ harness = false
247247
name = "take_fsl"
248248
harness = false
249249

250+
[[bench]]
251+
name = "take_varbin"
252+
harness = false
253+
250254
[[bench]]
251255
name = "take_filter"
252256
harness = false
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
#![expect(clippy::unwrap_used)]
5+
6+
use std::sync::LazyLock;
7+
8+
use divan::Bencher;
9+
use rand::RngExt;
10+
use rand::SeedableRng;
11+
use rand::rngs::StdRng;
12+
use vortex_array::IntoArray;
13+
use vortex_array::RecursiveCanonical;
14+
use vortex_array::VortexSessionExecute;
15+
use vortex_array::array_session;
16+
use vortex_array::arrays::VarBinArray;
17+
use vortex_array::dtype::DType;
18+
use vortex_array::dtype::Nullability;
19+
use vortex_buffer::Buffer;
20+
use vortex_session::VortexSession;
21+
22+
fn main() {
23+
LazyLock::force(&SESSION);
24+
divan::main();
25+
}
26+
27+
static SESSION: LazyLock<VortexSession> = LazyLock::new(array_session);
28+
29+
const ARRAY_SIZE: usize = 20_000;
30+
const TAKE_SIZE: usize = 8_000;
31+
32+
#[divan::bench]
33+
fn take_varbin(bencher: Bencher) {
34+
let array = VarBinArray::from_iter(
35+
(0..ARRAY_SIZE).map(|i| Some(format!("row-{i:0>40}"))),
36+
DType::Utf8(Nullability::NonNullable),
37+
)
38+
.into_array();
39+
40+
let mut rng = StdRng::seed_from_u64(0);
41+
let indices: Buffer<u64> = (0..TAKE_SIZE)
42+
.map(|_| rng.random_range(0..ARRAY_SIZE) as u64)
43+
.collect();
44+
let indices = indices.into_array();
45+
46+
bencher
47+
.with_inputs(|| (&array, &indices, SESSION.create_execution_ctx()))
48+
.bench_refs(|(array, indices, ctx)| {
49+
array
50+
.take((*indices).clone())
51+
.unwrap()
52+
.execute::<RecursiveCanonical>(ctx)
53+
});
54+
}

vortex-array/src/arrays/varbin/compute/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ mod filter;
1010
mod mask;
1111
mod take;
1212

13+
pub use take::take_varbin;
14+
1315
#[cfg(test)]
1416
mod tests {
1517
use rstest::rstest;

vortex-array/src/arrays/varbin/compute/take.rs

Lines changed: 136 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
// SPDX-License-Identifier: Apache-2.0
22
// SPDX-FileCopyrightText: Copyright the Vortex contributors
33

4+
use std::iter;
45
use std::ptr;
6+
use std::sync::Arc;
57

68
use itertools::Itertools as _;
9+
use num_traits::AsPrimitive;
710
use vortex_buffer::BitBufferMut;
11+
use vortex_buffer::Buffer;
812
use vortex_buffer::BufferMut;
913
use vortex_buffer::ByteBufferMut;
1014
use vortex_error::VortexExpect;
1115
use vortex_error::VortexResult;
1216
use vortex_error::vortex_ensure;
1317
use vortex_error::vortex_err;
1418
use vortex_error::vortex_panic;
19+
use vortex_mask::AllOr;
1520
use vortex_mask::Mask;
1621

1722
use crate::ArrayRef;
@@ -22,17 +27,21 @@ use crate::arrays::PiecewiseSequence;
2227
use crate::arrays::PrimitiveArray;
2328
use crate::arrays::VarBin;
2429
use crate::arrays::VarBinArray;
30+
use crate::arrays::VarBinViewArray;
2531
use crate::arrays::dict::TakeExecute;
2632
use crate::arrays::piecewise_sequence::constant_unsigned_usize;
2733
use crate::arrays::piecewise_sequence::maybe_contiguous_slices;
2834
use crate::arrays::primitive::PrimitiveArrayExt;
2935
use crate::arrays::varbin::VarBinArrayExt;
3036
use crate::arrays::varbin::VarBinArraySlotsExt;
37+
use crate::arrays::varbinview::BinaryView;
38+
use crate::arrays::varbinview::build_views::MAX_BUFFER_LEN;
3139
use crate::dtype::DType;
3240
use crate::dtype::IntegerPType;
3341
use crate::dtype::PType;
3442
use crate::dtype::UnsignedPType;
3543
use crate::executor::ExecutionCtx;
44+
use crate::match_each_integer_ptype;
3645
use crate::match_each_unsigned_integer_ptype;
3746
use crate::validity::Validity;
3847

@@ -138,88 +147,149 @@ impl TakeExecute for VarBin {
138147
indices: &ArrayRef,
139148
ctx: &mut ExecutionCtx,
140149
) -> VortexResult<Option<ArrayRef>> {
141-
if let Some(piecewise_indices) = indices.as_opt::<PiecewiseSequence>()
142-
&& let Some(taken) = take_contiguous_ranges(array, piecewise_indices, indices, ctx)?
143-
{
144-
return Ok(Some(taken));
150+
let offsets = array.offsets().clone().execute::<PrimitiveArray>(ctx)?;
151+
let offsets = offsets.reinterpret_cast(offsets.ptype().to_unsigned());
152+
let last_offset = match_each_unsigned_integer_ptype!(offsets.ptype(), |O| {
153+
offsets.as_slice::<O>().last().map_or(0usize, |&o| o.as_())
154+
});
155+
156+
// VarBinView can't hold this buffer, so we can't canonicalize and
157+
// take() (take panics). Convert to VarBin
158+
if last_offset > MAX_BUFFER_LEN {
159+
return Ok(Some(take_varbin(array, indices, ctx)?.into_array()));
145160
}
146161

147-
// TODO(joe): Be lazy with execute
148-
let offsets = array.offsets().clone().execute::<PrimitiveArray>(ctx)?;
149-
let data = array.bytes();
150-
let indices = indices.clone().execute::<PrimitiveArray>(ctx)?;
162+
let data = array.bytes().clone();
151163
let dtype = array
152164
.dtype()
153165
.clone()
154166
.union_nullability(indices.dtype().nullability());
155-
let array_validity = array
156-
.varbin_validity()
157-
.execute_mask(array.as_ref().len(), ctx)?;
158-
let indices_validity = indices
167+
let validity = array.validity()?.take(indices)?;
168+
169+
let indices = indices.clone().execute::<PrimitiveArray>(ctx)?;
170+
let indices_mask = indices
159171
.as_ref()
160172
.validity()?
161173
.execute_mask(indices.as_ref().len(), ctx)?;
162174

163-
// Offsets and indices are non-negative; read them through their unsigned reinterpretations
164-
// so we only monomorphize over the 4 unsigned widths each (4x4 instead of 8x8). On take,
165-
// offsets get widened to either 32- or 64-bit (to avoid overflow); the built output offsets
166-
// are reinterpreted back to `out_offset_ptype` to preserve the result's offset signedness.
167-
let out_offset_ptype = taken_offset_ptype(offsets.ptype());
168-
let offsets = offsets.reinterpret_cast(offsets.ptype().to_unsigned());
169-
let indices = indices.reinterpret_cast(indices.ptype().to_unsigned());
170-
171-
let array = match_each_unsigned_integer_ptype!(indices.ptype(), |I| {
172-
match offsets.ptype() {
173-
PType::U8 => take::<I, u8>(
174-
dtype,
175-
offsets.as_slice::<u8>(),
176-
data.as_slice(),
177-
indices.as_slice::<I>(),
178-
array_validity,
179-
indices_validity,
180-
out_offset_ptype,
181-
),
182-
PType::U16 => take::<I, u16>(
183-
dtype,
184-
offsets.as_slice::<u16>(),
185-
data.as_slice(),
186-
indices.as_slice::<I>(),
187-
array_validity,
188-
indices_validity,
189-
out_offset_ptype,
190-
),
191-
PType::U32 => take::<I, u32>(
192-
dtype,
193-
offsets.as_slice::<u32>(),
175+
let views = match_each_unsigned_integer_ptype!(offsets.ptype(), |O| {
176+
match_each_integer_ptype!(indices.ptype(), |I| {
177+
take_views(
178+
offsets.as_slice::<O>(),
194179
data.as_slice(),
195180
indices.as_slice::<I>(),
196-
array_validity,
197-
indices_validity,
198-
out_offset_ptype,
199-
),
200-
PType::U64 => take::<I, u64>(
201-
dtype,
202-
offsets.as_slice::<u64>(),
203-
data.as_slice(),
204-
indices.as_slice::<I>(),
205-
array_validity,
206-
indices_validity,
207-
out_offset_ptype,
208-
),
209-
_ => unreachable!("invalid PType for offsets"),
210-
}
181+
&indices_mask,
182+
)
183+
})
211184
});
212185

213-
Ok(Some(array?.into_array()))
186+
// SAFETY: every view references buffer 0 which is inside shared data buffer
187+
unsafe {
188+
Ok(Some(
189+
VarBinViewArray::new_unchecked(views, Arc::from([data]), dtype, validity)
190+
.into_array(),
191+
))
192+
}
214193
}
215194
}
216195

196+
fn take_views<O: UnsignedPType, I: IntegerPType + AsPrimitive<usize>>(
197+
offsets: &[O],
198+
data: &[u8],
199+
indices: &[I],
200+
mask: &Mask,
201+
) -> Buffer<BinaryView> {
202+
let build = |idx: usize| -> BinaryView {
203+
let start: usize = offsets[idx].as_();
204+
let stop: usize = offsets[idx + 1].as_();
205+
let value = &data[start..stop];
206+
let len = stop - start;
207+
208+
// Caller guarantees every offset is <= MAX_BUFFER_LEN
209+
let start: u32 = start.as_();
210+
if len > BinaryView::MAX_INLINED_SIZE {
211+
let mut prefix = [0u8; 4];
212+
prefix.copy_from_slice(&value[..4]);
213+
let len: u32 = len.as_();
214+
BinaryView::new_ref(len, prefix, 0, start)
215+
} else {
216+
BinaryView::make_view(value, 0, start)
217+
}
218+
};
219+
220+
match mask.bit_buffer() {
221+
AllOr::All => Buffer::from_trusted_len_iter(indices.iter().map(|i| build(i.as_()))),
222+
AllOr::None => {
223+
Buffer::from_trusted_len_iter(iter::repeat_n(BinaryView::default(), indices.len()))
224+
}
225+
AllOr::Some(buffer) => {
226+
Buffer::from_trusted_len_iter(buffer.iter().zip(indices.iter()).map(|(valid, i)| {
227+
if valid {
228+
build(i.as_())
229+
} else {
230+
BinaryView::default()
231+
}
232+
}))
233+
}
234+
}
235+
}
236+
237+
/// Take from a VarBin. Referenced bytes are copied
238+
pub fn take_varbin(
239+
array: ArrayView<'_, VarBin>,
240+
indices: &ArrayRef,
241+
ctx: &mut ExecutionCtx,
242+
) -> VortexResult<VarBinArray> {
243+
if let Some(piecewise_indices) = indices.as_opt::<PiecewiseSequence>()
244+
&& let Some(taken) = take_contiguous_ranges(array, piecewise_indices, indices, ctx)?
245+
{
246+
return Ok(taken);
247+
}
248+
249+
let offsets = array.offsets().clone().execute::<PrimitiveArray>(ctx)?;
250+
let data = array.bytes();
251+
let indices = indices.clone().execute::<PrimitiveArray>(ctx)?;
252+
let dtype = array
253+
.dtype()
254+
.clone()
255+
.union_nullability(indices.dtype().nullability());
256+
let array_validity = array
257+
.varbin_validity()
258+
.execute_mask(array.as_ref().len(), ctx)?;
259+
let indices_validity = indices
260+
.as_ref()
261+
.validity()?
262+
.execute_mask(indices.as_ref().len(), ctx)?;
263+
264+
// Offsets and indices are non-negative; read them through their unsigned reinterpretations
265+
// so we only monomorphize over the 4 unsigned widths each (4x4 instead of 8x8). On take,
266+
// offsets get widened to either 32- or 64-bit (to avoid overflow); the built output offsets
267+
// are reinterpreted back to `out_offset_ptype` to preserve the result's offset signedness.
268+
let out_offset_ptype = taken_offset_ptype(offsets.ptype());
269+
let offsets = offsets.reinterpret_cast(offsets.ptype().to_unsigned());
270+
let indices = indices.reinterpret_cast(indices.ptype().to_unsigned());
271+
272+
match_each_unsigned_integer_ptype!(indices.ptype(), |I| {
273+
match_each_unsigned_integer_ptype!(offsets.ptype(), |O| {
274+
take::<I, O>(
275+
dtype,
276+
offsets.as_slice::<O>(),
277+
data.as_slice(),
278+
indices.as_slice::<I>(),
279+
array_validity,
280+
indices_validity,
281+
out_offset_ptype,
282+
)
283+
})
284+
})
285+
}
286+
217287
fn take_contiguous_ranges(
218288
array: ArrayView<'_, VarBin>,
219289
indices: ArrayView<'_, PiecewiseSequence>,
220290
indices_ref: &ArrayRef,
221291
ctx: &mut ExecutionCtx,
222-
) -> VortexResult<Option<ArrayRef>> {
292+
) -> VortexResult<Option<VarBinArray>> {
223293
let Some((starts, lengths)) = maybe_contiguous_slices(indices, ctx)? else {
224294
return Ok(None);
225295
};
@@ -261,10 +331,12 @@ fn take_contiguous_ranges(
261331
// SAFETY: output offsets are built from valid input offsets, start at zero, are monotonically
262332
// non-decreasing, and the copied data buffer has exactly the referenced byte length.
263333
unsafe {
264-
Ok(Some(
265-
VarBinArray::new_unchecked(result.offsets, result.data.freeze(), dtype, validity)
266-
.into_array(),
267-
))
334+
Ok(Some(VarBinArray::new_unchecked(
335+
result.offsets,
336+
result.data.freeze(),
337+
dtype,
338+
validity,
339+
)))
268340
}
269341
}
270342

vortex-array/src/arrays/varbin/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ pub use array::VarBinSlotsView;
1111
pub use vtable::VarBinArray;
1212

1313
pub(crate) mod compute;
14+
pub use compute::take_varbin;
1415

1516
mod vtable;
1617
pub use vtable::VarBin;

vortex-array/src/arrays/varbin/vtable/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ use crate::dtype::PType;
3434
use crate::match_each_varbin_builder;
3535
use crate::serde::ArrayChildren;
3636
use crate::validity::Validity;
37-
mod canonical;
37+
pub(crate) mod canonical;
3838
mod kernel;
3939
mod operations;
4040
mod validity;

0 commit comments

Comments
 (0)