Skip to content

Commit e269163

Browse files
authored
fix: performance regression for filtering ListView (#5390)
In #4946, we forced rebuilding ListView arrays that were being built. A user report came in with a 10x performance regression with lots of time being spent inside of ZSTD decompression. On further investigation, the flame graph clarified that inside of a file scan, we were filtering a ListView array, where the elements were ZSTD compressed. Calling `naive_rebuild` goes through an awful `append_scalar` pathway, and `scalar_at` for ZSTD...decompresses a whole frame. To avoid this, we fully canonicalize the elements, offsets, and sizes in bulk, then stitch a new ListView from the components. This is 10-20x faster than the previous codepath, per the added benchmark. Before: ``` Timer precision: 41 ns listview_rebuild fastest │ slowest │ median │ mean │ samples │ iters ╰─ rebuild_naive 1.821 ms │ 2.535 ms │ 2.019 ms │ 2.024 ms │ 100 │ 100 ``` After: ``` Timer precision: 41 ns listview_rebuild fastest │ slowest │ median │ mean │ samples │ iters ╰─ rebuild_naive 109.5 µs │ 192.2 µs │ 121.1 µs │ 122 µs │ 100 │ 100 ``` --------- Signed-off-by: Andrew Duffy <[email protected]>
1 parent 60492c4 commit e269163

File tree

5 files changed

+105
-39
lines changed

5 files changed

+105
-39
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

encodings/zstd/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,10 @@ vortex-vector = { workspace = true }
3030
zstd = { workspace = true }
3131

3232
[dev-dependencies]
33+
divan = { workspace = true }
3334
rstest = { workspace = true }
3435
vortex-array = { workspace = true, features = ["test-harness"] }
36+
37+
[[bench]]
38+
name = "listview_rebuild"
39+
harness = false
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
#![allow(clippy::unwrap_used)]
5+
6+
use divan::Bencher;
7+
use vortex_array::IntoArray;
8+
use vortex_array::arrays::{ListViewArray, ListViewRebuildMode, VarBinViewArray};
9+
use vortex_array::validity::Validity;
10+
use vortex_buffer::Buffer;
11+
use vortex_zstd::ZstdArray;
12+
13+
#[divan::bench]
14+
fn rebuild_naive(bencher: Bencher) {
15+
let dudes = VarBinViewArray::from_iter_str(["Washington", "Adams", "Jefferson", "Madison"])
16+
.into_array();
17+
let dudes = ZstdArray::from_array(dudes, 9, 1024).unwrap().into_array();
18+
19+
let offsets = std::iter::repeat_n(0u32, 1024)
20+
.collect::<Buffer<u32>>()
21+
.into_array();
22+
let sizes = [0u64, 1, 2, 3, 4]
23+
.into_iter()
24+
.cycle()
25+
.take(1024)
26+
.collect::<Buffer<u64>>()
27+
.into_array();
28+
29+
let list_view = ListViewArray::new(dudes, offsets, sizes, Validity::NonNullable);
30+
31+
bencher.bench_local(|| list_view.rebuild(ListViewRebuildMode::MakeZeroCopyToList))
32+
}
33+
34+
fn main() {
35+
divan::main()
36+
}

vortex-array/src/arrays/listview/rebuild.rs

Lines changed: 63 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
// SPDX-FileCopyrightText: Copyright the Vortex contributors
33

44
use num_traits::FromPrimitive;
5+
use vortex_buffer::BufferMut;
56
use vortex_dtype::{IntegerPType, Nullability, match_each_integer_ptype};
67
use vortex_error::VortexExpect;
78
use vortex_scalar::Scalar;
89

9-
use crate::arrays::ListViewArray;
10-
use crate::builders::{ArrayBuilder, ListViewBuilder};
10+
use crate::arrays::{ChunkedArray, ListViewArray};
1111
use crate::vtable::ValidityHelper;
12-
use crate::{Array, compute};
12+
use crate::{Array, IntoArray, ToCanonical, compute};
1313

1414
/// Modes for rebuilding a [`ListViewArray`].
1515
pub enum ListViewRebuildMode {
@@ -74,35 +74,80 @@ impl ListViewArray {
7474
let offsets_ptype = self.offsets().dtype().as_ptype();
7575
let sizes_ptype = self.sizes().dtype().as_ptype();
7676

77-
match_each_integer_ptype!(offsets_ptype, |O| {
78-
match_each_integer_ptype!(sizes_ptype, |S| { self.naive_rebuild::<O, S>() })
77+
match_each_integer_ptype!(sizes_ptype, |S| {
78+
match offsets_ptype {
79+
PType::U8 => self.naive_rebuild::<u8, u32, S>(),
80+
PType::U16 => self.naive_rebuild::<u16, u32, S>(),
81+
PType::U32 => self.naive_rebuild::<u32, u32, S>(),
82+
PType::U64 => self.naive_rebuild::<u64, u64, S>(),
83+
PType::I8 => self.naive_rebuild::<i8, i32, S>(),
84+
PType::I16 => self.naive_rebuild::<i16, i32, S>(),
85+
PType::I32 => self.naive_rebuild::<i32, i32, S>(),
86+
PType::I64 => self.naive_rebuild::<i64, i64, S>(),
87+
_ => unreachable!("invalid offsets PType"),
88+
}
7989
})
8090
}
8191

8292
/// The inner function for `rebuild_zero_copy_to_list`, which naively rebuilds a `ListViewArray`
8393
/// via `append_scalar`.
84-
fn naive_rebuild<O: IntegerPType, S: IntegerPType>(&self) -> ListViewArray {
94+
fn naive_rebuild<O: IntegerPType, NewOffset: IntegerPType, S: IntegerPType>(
95+
&self,
96+
) -> ListViewArray {
8597
let element_dtype = self
8698
.dtype()
8799
.as_list_element_opt()
88100
.vortex_expect("somehow had a canonical list that was not a list");
89101

90-
let mut builder = ListViewBuilder::<O, S>::with_capacity(
91-
element_dtype.clone(),
92-
self.dtype().nullability(),
93-
self.elements().len(),
94-
self.len(),
95-
);
102+
// Upfront canonicalize the list elements, we're going to be doing a lot of
103+
// slicing with them.
104+
let elements_canonical = self.elements().to_canonical().into_array();
105+
let offsets_canonical = self.offsets().to_primitive();
106+
let sizes_canonical = self.sizes().to_primitive();
107+
108+
let offsets_canonical = offsets_canonical.as_slice::<O>();
109+
let sizes_canonical = sizes_canonical.as_slice::<S>();
110+
111+
let mut offsets = BufferMut::<NewOffset>::with_capacity(self.len());
112+
let mut sizes = BufferMut::<S>::with_capacity(self.len());
113+
114+
let mut chunks = Vec::with_capacity(self.len());
115+
116+
let mut n_elements = NewOffset::zero();
96117

97-
for i in 0..self.len() {
98-
let list = self.scalar_at(i);
118+
for index in 0..self.len() {
119+
if !self.is_valid(index) {
120+
offsets.push(offsets.last().copied().unwrap_or_default());
121+
sizes.push(S::zero());
122+
continue;
123+
}
99124

100-
builder
101-
.append_scalar(&list)
102-
.vortex_expect("was unable to extend the `ListViewBuilder`")
125+
let offset = offsets_canonical[index];
126+
let size = sizes_canonical[index];
127+
128+
let start = offset.as_();
129+
let stop = start + size.as_();
130+
131+
chunks.push(elements_canonical.slice(start..stop));
132+
offsets.push(n_elements);
133+
sizes.push(size);
134+
135+
n_elements += num_traits::cast(size).vortex_expect("cast");
103136
}
104137

105-
builder.finish_into_listview()
138+
let offsets = offsets.into_array();
139+
let sizes = sizes.into_array();
140+
141+
// SAFETY: all chunks were sliced from the same array so have same DType.
142+
let elements =
143+
unsafe { ChunkedArray::new_unchecked(chunks, element_dtype.as_ref().clone()) };
144+
145+
ListViewArray::new(
146+
elements.to_canonical().into_array(),
147+
offsets,
148+
sizes,
149+
self.validity.clone(),
150+
)
106151
}
107152

108153
/// Rebuilds a [`ListViewArray`] by trimming any unused / unreferenced leading and trailing

vortex-array/src/builders/listview.rs

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -77,17 +77,6 @@ impl<O: IntegerPType, S: IntegerPType> ListViewBuilder<O, S> {
7777
elements_capacity: usize,
7878
capacity: usize,
7979
) -> Self {
80-
// Validate that size type's maximum value fits within offset type's maximum value.
81-
// Since offsets are non-negative, we only need to check max values.
82-
assert!(
83-
S::max_value_as_u64() <= O::max_value_as_u64(),
84-
"Size type {:?} (max offset {}) must fit within offset type {:?} (max offset {})",
85-
S::PTYPE,
86-
S::max_value_as_u64(),
87-
O::PTYPE,
88-
O::max_value_as_u64()
89-
);
90-
9180
let elements_builder = builder_with_capacity(&element_dtype, elements_capacity);
9281

9382
let offsets_builder =
@@ -628,14 +617,4 @@ mod tests {
628617
.contains("null value to non-nullable")
629618
);
630619
}
631-
632-
#[test]
633-
#[should_panic(
634-
expected = "Size type I32 (max offset 2147483647) must fit within offset type I16 (max offset 32767)"
635-
)]
636-
fn test_error_invalid_type_combination() {
637-
let dtype: Arc<DType> = Arc::new(I32.into());
638-
// This should panic because i32 (4 bytes) cannot fit within i16 (2 bytes).
639-
let _builder = ListViewBuilder::<i16, i32>::with_capacity(dtype, NonNullable, 0, 0);
640-
}
641620
}

0 commit comments

Comments
 (0)