Skip to content

Commit c1375bb

Browse files
committed
Use an operation-owned indexed probe cache for new-prefix arrays
Signed-off-by: Daniel King <dan@spiraldb.com>
1 parent f5d2f4f commit c1375bb

6 files changed

Lines changed: 199 additions & 7 deletions

File tree

‎encodings/pco/src/access.rs‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,21 @@ impl AccessState {
161161
mask: cached.mask.mask.slice(start_in_page..end_in_page),
162162
})
163163
}
164+
pub fn page_id(&self) -> Option<usize> {
165+
self.last.as_ref().map(|(page, _)| *page)
166+
}
167+
pub fn cached_position(&self, row: usize) -> Option<Option<(usize, usize)>> {
168+
let (page, cached) = self.last.as_ref()?;
169+
if !cached.rows.contains(&row) {
170+
return None;
171+
}
172+
Some(
173+
cached
174+
.mask
175+
.value_index::<true>(row - cached.rows.start)
176+
.map(|value| (*page, value)),
177+
)
178+
}
164179
}
165180
pub(crate) struct PageSpan {
166181
pub page: usize,

‎encodings/pco/src/array.rs‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ use crate::PcoLogicalRowPrefix;
6666
use crate::PcoMetadata;
6767
use crate::PcoPageInfo;
6868
use crate::page_index::PageIndex;
69+
use crate::probe::PcoProbeState;
6970
use crate::take::slice as decode_slice;
7071

7172
// Overall approach here:
@@ -839,7 +840,7 @@ impl ValidityVTable<Pco> for Pco {
839840
}
840841

841842
impl OperationsVTable<Pco> for Pco {
842-
type ProbeState = crate::probe::PcoProbeState;
843+
type ProbeState = PcoProbeState;
843844

844845
fn probe_scalar(
845846
state: &mut ProbeState<'_, Pco>,

‎encodings/pco/src/decoder.rs‎

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ pub(crate) struct DecodedPages<T: Number> {
117117
decoder: PageDecoder<T>,
118118
lookup: PageLookup,
119119
pages: BTreeMap<usize, PrimitiveArray>,
120+
last: Option<(usize, PrimitiveArray)>,
120121
}
121122

122123
impl<T: Number + NativePType> DecodedPages<T> {
@@ -125,18 +126,26 @@ impl<T: Number + NativePType> DecodedPages<T> {
125126
decoder: PageDecoder::new(array.metadata.header.as_slice())?,
126127
lookup: PageLookup::default(),
127128
pages: BTreeMap::new(),
129+
last: None,
128130
})
129131
}
132+
pub fn last_values(&self, page: usize) -> Option<&PrimitiveArray> {
133+
self.last
134+
.as_ref()
135+
.filter(|(last, _)| *last == page)
136+
.map(|(_, values)| values)
137+
}
130138
pub fn values(
131139
&mut self,
132140
view: &LogicalPageIndex,
133141
array: ArrayView<'_, Pco>,
134142
page: usize,
135143
ctx: &mut ExecutionCtx,
136144
) -> VortexResult<&PrimitiveArray> {
137-
let entry = match self.pages.entry(page) {
138-
Entry::Occupied(entry) => entry.into_mut(),
139-
Entry::Vacant(entry) => {
145+
if !self.last.as_ref().is_some_and(|(last, _)| *last == page) {
146+
let decoded = if let Some(decoded) = self.pages.remove(&page) {
147+
decoded
148+
} else {
140149
let (chunk_id, n_values) = self.lookup.resolve(view, array, page)?;
141150
let buffer = array
142151
.pages
@@ -146,15 +155,23 @@ impl<T: Number + NativePType> DecodedPages<T> {
146155
.chunk_metas
147156
.get(chunk_id)
148157
.ok_or_else(|| vortex_err!("Pco chunk metadata is missing"))?;
149-
entry.insert(self.decoder.read(
158+
self.decoder.read(
150159
chunk_id,
151160
chunk.as_slice(),
152161
n_values,
153162
buffer.as_slice(),
154163
ctx,
155-
)?)
164+
)?
165+
};
166+
if let Some((page, previous)) = self.last.replace((page, decoded)) {
167+
self.pages.insert(page, previous);
156168
}
157-
};
169+
}
170+
let entry = &self
171+
.last
172+
.as_ref()
173+
.vortex_expect("decoded page prepared above")
174+
.1;
158175
Ok(entry)
159176
}
160177
}

‎encodings/pco/src/indexed_probe.rs‎

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
//! Operation-owned probes bind indexed or legacy addressing on first use.
5+
6+
use std::collections::BTreeMap;
7+
use std::mem::replace;
8+
use std::sync::Arc;
9+
10+
use pco::data_types::Number;
11+
use pco::data_types::NumberType;
12+
use pco::match_number_enum;
13+
use vortex_array::ArrayView;
14+
use vortex_array::ExecutionCtx;
15+
use vortex_array::dtype::NativePType;
16+
use vortex_array::dtype::half;
17+
use vortex_array::scalar::PValue;
18+
use vortex_array::scalar::Scalar;
19+
use vortex_error::VortexExpect;
20+
use vortex_error::VortexResult;
21+
22+
use crate::Pco;
23+
use crate::access::AccessState;
24+
use crate::array::number_type_from_ptype;
25+
use crate::decoder::DecodedPages;
26+
use crate::page_index::LogicalPageIndex;
27+
use crate::page_index::PageIndex;
28+
29+
/// Construction is empty; first use binds an owned immutable page-index variant and
30+
/// typed decoder. Touched decoded pages can be retained up to the entire source array.
31+
#[derive(Default)]
32+
pub(crate) struct IndexedProbeState {
33+
bound: Option<Box<dyn BoundProbe>>,
34+
}
35+
36+
trait BoundProbe: Send + Sync {
37+
fn scalar(
38+
&mut self,
39+
array: ArrayView<'_, Pco>,
40+
row: usize,
41+
ranked: bool,
42+
ctx: &mut ExecutionCtx,
43+
) -> VortexResult<Scalar>;
44+
}
45+
46+
struct TypedProbe<T: Number> {
47+
owner: Arc<LogicalPageIndex>,
48+
access: AccessState,
49+
prepared: BTreeMap<usize, AccessState>,
50+
decoded: Option<DecodedPages<T>>,
51+
}
52+
53+
impl<T> BoundProbe for TypedProbe<T>
54+
where
55+
T: Number + NativePType + Into<PValue>,
56+
{
57+
fn scalar(
58+
&mut self,
59+
array: ArrayView<'_, Pco>,
60+
row: usize,
61+
ranked: bool,
62+
ctx: &mut ExecutionCtx,
63+
) -> VortexResult<Scalar> {
64+
if ranked && let Some(position) = self.access.cached_position(row) {
65+
match position {
66+
None => return Ok(Scalar::null(array.dtype().clone())),
67+
Some((page, value)) => {
68+
if let Some(values) = self
69+
.decoded
70+
.as_ref()
71+
.and_then(|decoded| decoded.last_values(page))
72+
{
73+
return Ok(Scalar::primitive(
74+
values.as_slice::<T>()[value],
75+
array.dtype().nullability(),
76+
));
77+
}
78+
}
79+
}
80+
}
81+
let view = self.owner.as_ref();
82+
let page = view.find_row(row);
83+
if self.access.page_id() != Some(page) {
84+
let next = self.prepared.remove(&page).unwrap_or_default();
85+
let previous = replace(&mut self.access, next);
86+
if let Some(previous_page) = previous.page_id() {
87+
self.prepared.insert(previous_page, previous);
88+
}
89+
}
90+
let position = if ranked {
91+
self.access.resolve::<true>(view, array, row, ctx)?
92+
} else {
93+
self.access.resolve::<false>(view, array, row, ctx)?
94+
};
95+
let Some((page, value)) = position else {
96+
return Ok(Scalar::null(array.dtype().clone()));
97+
};
98+
if self.decoded.is_none() {
99+
self.decoded = Some(DecodedPages::new(array)?);
100+
}
101+
let decoded = self
102+
.decoded
103+
.as_mut()
104+
.vortex_expect("decoder prepared above");
105+
let values = decoded.values(view, array, page, ctx)?;
106+
Ok(Scalar::primitive(
107+
values.as_slice::<T>()[value],
108+
array.dtype().nullability(),
109+
))
110+
}
111+
}
112+
113+
fn bind<T: Number + NativePType + Into<PValue>>(owner: Arc<LogicalPageIndex>) -> Box<dyn BoundProbe> {
114+
Box::new(TypedProbe::<T> {
115+
owner,
116+
access: AccessState::default(),
117+
prepared: BTreeMap::new(),
118+
decoded: None,
119+
})
120+
}
121+
122+
pub(crate) fn scalar_at(
123+
array: ArrayView<'_, Pco>,
124+
index: usize,
125+
state: Option<&mut IndexedProbeState>,
126+
ctx: &mut ExecutionCtx,
127+
) -> VortexResult<Scalar> {
128+
// The public probe dispatcher already checks visible bounds. A zero-page array
129+
// stores no defined values, so no validity/index/decoder preparation is needed.
130+
if array.pages.is_empty() {
131+
return Ok(Scalar::null(array.dtype().clone()));
132+
}
133+
let mut once = IndexedProbeState::default();
134+
let ranked = state.is_some();
135+
let state = state.unwrap_or(&mut once);
136+
if state.bound.is_none() {
137+
let PageIndex::IndexedFast(owner) = array.page_index()? else {
138+
unreachable!("indexed probes are dispatched only for a present prefix")
139+
};
140+
state.bound = Some(
141+
match_number_enum!(number_type_from_ptype(array.dtype().as_ptype()), NumberType<T> => {
142+
bind::<T>(Arc::clone(owner))
143+
}),
144+
);
145+
}
146+
state.bound.as_mut().vortex_expect("bound above").scalar(
147+
array,
148+
array.slice_start() + index,
149+
ranked,
150+
ctx,
151+
)
152+
}

‎encodings/pco/src/lib.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ mod access;
3232
mod array;
3333
mod compute;
3434
mod decoder;
35+
mod indexed_probe;
3536
mod page_index;
3637
mod probe;
3738
mod rules;

‎encodings/pco/src/probe.rs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ use crate::Pco;
2727
use crate::PcoArrayExt;
2828
use crate::array::number_type_from_ptype;
2929
use crate::array::vortex_err_from_pco;
30+
use crate::indexed_probe::IndexedProbeState;
31+
use crate::indexed_probe::scalar_at as indexed_scalar_at;
3032

3133
const RANK_STRIDE: usize = 512;
3234

@@ -37,6 +39,7 @@ const RANK_STRIDE: usize = 512;
3739
/// and never beyond the decompressed array.
3840
#[derive(Default)]
3941
pub struct PcoProbeState {
42+
indexed: IndexedProbeState,
4043
validity: Option<Mask>,
4144
rank: Vec<usize>,
4245
pages: Vec<Page>,
@@ -56,6 +59,9 @@ pub(crate) fn scalar_at(
5659
state: Option<&mut PcoProbeState>,
5760
ctx: &mut ExecutionCtx,
5861
) -> VortexResult<Scalar> {
62+
if array.metadata.logical_row_prefix.is_some() {
63+
return indexed_scalar_at(array, index, state.map(|state| &mut state.indexed), ctx);
64+
}
5965
let Some(state) = state else {
6066
let validity = array.unsliced_validity();
6167
return array

0 commit comments

Comments
 (0)