arrow_buffer/buffer/run.rs
1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements. See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership. The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License. You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied. See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use crate::ArrowNativeType;
19use crate::buffer::ScalarBuffer;
20
21/// A buffer of monotonically increasing, positive integers used to store run-ends.
22///
23/// Used to compactly represent runs of the same value. Values being represented
24/// are stored in a separate buffer from this struct. See [`RunArray`] for an example
25/// of how this is used with a companion array to represent the values.
26///
27/// # Logical vs Physical
28///
29/// Physically, each value in the `run_ends` buffer is the cumulative length of
30/// all runs in the logical representation, up to that physical index. Consider
31/// the following example:
32///
33/// ```text
34/// physical logical
35/// ┌─────────┬─────────┐ ┌─────────┬─────────┐
36/// │ 3 │ 0 │ ◄──────┬─ │ A │ 0 │
37/// ├─────────┼─────────┤ │ ├─────────┼─────────┤
38/// │ 4 │ 1 │ ◄────┐ ├─ │ A │ 1 │
39/// ├─────────┼─────────┤ │ │ ├─────────┼─────────┤
40/// │ 6 │ 2 │ ◄──┐ │ └─ │ A │ 2 │
41/// └─────────┴─────────┘ │ │ ├─────────┼─────────┤
42/// run-ends index │ └─── │ B │ 3 │
43/// │ ├─────────┼─────────┤
44/// logical_offset = 0 ├───── │ C │ 4 │
45/// logical_length = 6 │ ├─────────┼─────────┤
46/// └───── │ C │ 5 │
47/// └─────────┴─────────┘
48/// values index
49/// ```
50///
51/// A [`RunEndBuffer`] is physically the buffer and offset with length on the left.
52/// In this case, the offset and length represent the whole buffer, so it is essentially
53/// unsliced. See the section below on slicing for more details on how this buffer
54/// handles slicing.
55///
56/// This means that multiple logical values are represented in the same physical index,
57/// and multiple logical indices map to the same physical index. The [`RunEndBuffer`]
58/// containing `[3, 4, 6]` is essentially the physical indices `[0, 0, 0, 1, 2, 2]`,
59/// and having a separately stored buffer of values such as `[A, B, C]` can turn
60/// this into a representation of `[A, A, A, B, C, C]`.
61///
62/// # Slicing
63///
64/// In order to provide zero-copy slicing, this struct stores a separate **logical**
65/// offset and length. Consider the following example:
66///
67/// ```text
68/// physical logical
69/// ┌─────────┬─────────┐ ┌ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ┐
70/// │ 3 │ 0 │ ◄──────┐ A 0
71/// ├─────────┼─────────┤ │ ├── ─ ─ ─ ┼ ─ ─ ─ ─ ┤
72/// │ 4 │ 1 │ ◄────┐ │ A 1
73/// ├─────────┼─────────┤ │ │ ├─────────┼─────────┤
74/// │ 6 │ 2 │ ◄──┐ │ └─ │ A │ 2 │◄─── logical_offset
75/// └─────────┴─────────┘ │ │ ├─────────┼─────────┤
76/// run-ends index │ └─── │ B │ 3 │
77/// │ ├─────────┼─────────┤
78/// logical_offset = 2 └───── │ C │ 4 │
79/// logical_length = 3 ├─────────┼─────────┤
80/// C 5 ◄─── logical_offset + logical_length
81/// └ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ┘
82/// values index
83/// ```
84///
85/// The physical `run_ends` [`ScalarBuffer`] remains unchanged, in order to facilitate
86/// zero-copy. However, we now offset into the **logical** representation with an
87/// accompanying length. This allows us to represent values `[A, B, C]` using physical
88/// indices `0, 1, 2` with the same underlying physical buffer, at the cost of two
89/// extra `usize`s to represent the logical slice that was taken.
90///
91/// (A [`RunEndBuffer`] is considered unsliced when `logical_offset` is `0` and
92/// `logical_length` is equal to the last value in `run_ends`)
93///
94/// [`RunArray`]: https://docs.rs/arrow/latest/arrow/array/struct.RunArray.html
95/// [Run-End encoded layout]: https://arrow.apache.org/docs/format/Columnar.html#run-end-encoded-layout
96#[derive(Debug, Clone)]
97pub struct RunEndBuffer<E: ArrowNativeType> {
98 run_ends: ScalarBuffer<E>,
99 logical_length: usize,
100 logical_offset: usize,
101}
102
103impl<E> RunEndBuffer<E>
104where
105 E: ArrowNativeType,
106{
107 /// Create a new [`RunEndBuffer`] from a [`ScalarBuffer`], `logical_offset`
108 /// and `logical_length`.
109 ///
110 /// # Panics
111 ///
112 /// Panics if the [`RunArray` invariants] are not satisfied:
113 ///
114 /// - `run_ends` does not contain strictly increasing values greater than zero
115 /// - `logical_length` is not 0 and the last value of `run_ends` is less than
116 /// `logical_offset + logical_length` (an empty slice is always accepted)
117 ///
118 /// [`RunArray` invariants]: https://docs.rs/arrow/latest/arrow/array/struct.RunArray.html#offset-and-len
119 pub fn new(run_ends: ScalarBuffer<E>, logical_offset: usize, logical_length: usize) -> Self {
120 assert!(
121 run_ends.windows(2).all(|w| w[0] < w[1]),
122 "run-ends not strictly increasing"
123 );
124
125 if logical_length != 0 {
126 assert!(!run_ends.is_empty(), "non-empty slice but empty run-ends");
127 let end = E::from_usize(logical_offset.saturating_add(logical_length)).unwrap();
128 assert!(
129 *run_ends.first().unwrap() > E::usize_as(0),
130 "run-ends not greater than 0"
131 );
132 assert!(
133 *run_ends.last().unwrap() >= end,
134 "slice beyond bounds of run-ends"
135 );
136 }
137
138 Self {
139 run_ends,
140 logical_length,
141 logical_offset,
142 }
143 }
144
145 /// Create a new [`RunEndBuffer`] from a [`ScalarBuffer`], `logical_offset`
146 /// and `logical_length`.
147 ///
148 /// # Safety
149 ///
150 /// `run_ends` must satisfy the [`RunArray` invariants]:
151 ///
152 /// - `run_ends` must contain strictly increasing values greater than zero
153 /// - If `logical_length` is not 0, the last value of `run_ends` must be
154 /// greater than or equal to `logical_offset + logical_length`
155 ///
156 /// [`RunArray` invariants]: https://docs.rs/arrow/latest/arrow/array/struct.RunArray.html#offset-and-len
157 pub unsafe fn new_unchecked(
158 run_ends: ScalarBuffer<E>,
159 logical_offset: usize,
160 logical_length: usize,
161 ) -> Self {
162 Self {
163 run_ends,
164 logical_length,
165 logical_offset,
166 }
167 }
168
169 /// Returns the logical offset into the run-ends stored by this buffer.
170 #[inline]
171 pub fn offset(&self) -> usize {
172 self.logical_offset
173 }
174
175 /// Returns the logical length of the run-ends stored by this buffer.
176 #[inline]
177 pub fn len(&self) -> usize {
178 self.logical_length
179 }
180
181 /// Returns true if this buffer is logically empty.
182 #[inline]
183 pub fn is_empty(&self) -> bool {
184 self.logical_length == 0
185 }
186
187 /// Free up unused memory.
188 pub fn shrink_to_fit(&mut self) {
189 // TODO(emilk): we could shrink even more in the case where we are a small sub-slice of the full buffer
190 self.run_ends.shrink_to_fit();
191 }
192
193 /// Returns the physical (**unsliced**) run ends of this buffer.
194 ///
195 /// Take care when operating on these values as it doesn't take into account
196 /// any logical slicing that may have occurred.
197 #[inline]
198 pub fn values(&self) -> &[E] {
199 &self.run_ends
200 }
201
202 /// Returns an iterator yielding run ends adjusted for the logical slice.
203 ///
204 /// Each yielded value is subtracted by the [`logical_offset`] and capped
205 /// at the [`logical_length`].
206 ///
207 /// [`logical_offset`]: Self::offset
208 /// [`logical_length`]: Self::len
209 pub fn sliced_values(&self) -> impl Iterator<Item = E> + '_ {
210 let offset = self.logical_offset;
211 let len = self.logical_length;
212 // Doing this roundabout way since the iterator type we return must be
213 // the same (i.e. cannot use std::iter::empty())
214 let physical_slice = if self.is_empty() {
215 &self.run_ends[0..0]
216 } else {
217 let start = self.get_start_physical_index();
218 let end = self.get_end_physical_index();
219 &self.run_ends[start..=end]
220 };
221 physical_slice.iter().map(move |&val| {
222 // `len` is at most the largest run end, so it always fits in `E`
223 let val = val.as_usize().saturating_sub(offset).min(len);
224 E::usize_as(val)
225 })
226 }
227
228 /// Returns the maximum run-end encoded in the underlying buffer; that is, the
229 /// last physical run of the buffer. This does not take into account any logical
230 /// slicing that may have occurred.
231 #[inline]
232 pub fn max_value(&self) -> usize {
233 self.values().last().copied().unwrap_or_default().as_usize()
234 }
235
236 /// Performs a binary search to find the physical index for the given logical
237 /// index.
238 ///
239 /// Useful for extracting the corresponding physical `run_ends` when this buffer
240 /// is logically sliced.
241 ///
242 /// The result is arbitrary if `logical_index >= self.len()`.
243 pub fn get_physical_index(&self, logical_index: usize) -> usize {
244 let logical_index = self.logical_offset + logical_index;
245 let cmp = |p: &E| p.as_usize().cmp(&logical_index);
246
247 match self.run_ends.binary_search_by(cmp) {
248 Ok(idx) => idx + 1,
249 Err(idx) => idx,
250 }
251 }
252
253 /// Returns the physical index at which the logical array starts.
254 ///
255 /// The same as calling `get_physical_index(0)` but with a fast path if the
256 /// buffer is not logically sliced, in which case it always returns `0`.
257 pub fn get_start_physical_index(&self) -> usize {
258 if self.logical_offset == 0 || self.logical_length == 0 {
259 return 0;
260 }
261 // Fallback to binary search
262 self.get_physical_index(0)
263 }
264
265 /// Returns the physical index at which the logical array ends.
266 ///
267 /// The same as calling `get_physical_index(length - 1)` but with a fast path
268 /// if the buffer is not logically sliced, in which case it returns `length - 1`.
269 pub fn get_end_physical_index(&self) -> usize {
270 if self.logical_length == 0 {
271 return 0;
272 }
273 if self.max_value() == self.logical_offset + self.logical_length {
274 return self.values().len() - 1;
275 }
276 // Fallback to binary search
277 self.get_physical_index(self.logical_length - 1)
278 }
279
280 /// Slices this [`RunEndBuffer`] by the provided `logical_offset` and `logical_length`.
281 ///
282 /// # Panics
283 ///
284 /// - Specified slice (`logical_offset` + `logical_length`) exceeds existing
285 /// logical length
286 pub fn slice(&self, logical_offset: usize, logical_length: usize) -> Self {
287 assert!(
288 logical_offset.saturating_add(logical_length) <= self.logical_length,
289 "the length + offset of the sliced RunEndBuffer cannot exceed the existing length"
290 );
291 Self {
292 run_ends: self.run_ends.clone(),
293 logical_offset: self.logical_offset + logical_offset,
294 logical_length,
295 }
296 }
297
298 /// Returns the inner [`ScalarBuffer`].
299 pub fn inner(&self) -> &ScalarBuffer<E> {
300 &self.run_ends
301 }
302
303 /// Returns the inner [`ScalarBuffer`], consuming self.
304 pub fn into_inner(self) -> ScalarBuffer<E> {
305 self.run_ends
306 }
307
308 /// Claim memory used by this buffer in the provided memory pool.
309 #[cfg(feature = "pool")]
310 pub fn claim(&self, pool: &dyn crate::MemoryPool) {
311 self.run_ends.claim(pool);
312 }
313
314 /// Returns the physical indices corresponding to the provided logical indices.
315 ///
316 /// Given a slice of logical indices, this method returns a `Vec` containing the
317 /// corresponding physical indices into the run-ends buffer.
318 ///
319 /// This method operates by iterating the logical indices in sorted order, instead of
320 /// finding the physical index for each logical index using binary search via
321 /// the function [`RunEndBuffer::get_physical_index`].
322 ///
323 /// Running benchmarks on both approaches showed that the approach used here
324 /// scaled well for larger inputs.
325 ///
326 /// See <https://github.com/apache/arrow-rs/pull/3622#issuecomment-1407753727> for more details.
327 ///
328 /// # Errors
329 ///
330 /// If any logical index is out of bounds (>= self.len()), returns an error containing the invalid index.
331 #[inline]
332 pub fn get_physical_indices<I>(&self, logical_indices: &[I]) -> Result<Vec<usize>, I>
333 where
334 I: ArrowNativeType,
335 {
336 let len = self.len();
337 let offset = self.offset();
338
339 let indices_len = logical_indices.len();
340
341 if indices_len == 0 {
342 return Ok(vec![]);
343 }
344
345 // `ordered_indices` store index into `logical_indices` and can be used
346 // to iterate `logical_indices` in sorted order.
347 let mut ordered_indices: Vec<usize> = (0..indices_len).collect();
348
349 // Instead of sorting `logical_indices` directly, sort the `ordered_indices`
350 // whose values are index of `logical_indices`
351 ordered_indices.sort_unstable_by_key(|&idx| logical_indices[idx].as_usize());
352
353 // Return early if all the logical indices cannot be converted to physical indices.
354 // `ordered_indices` has `indices_len` entries, and the empty case returned above.
355 let largest_logical_index = logical_indices[ordered_indices[indices_len - 1]];
356 if largest_logical_index.as_usize() >= len {
357 return Err(largest_logical_index);
358 }
359
360 // Skip some physical indices based on offset.
361 let skip_value = self.get_start_physical_index();
362
363 let mut physical_indices = vec![0; indices_len];
364
365 let mut ordered_index = 0_usize;
366 for (physical_index, run_end) in self.values().iter().enumerate().skip(skip_value) {
367 // Get the run end index (relative to offset) of current physical index
368 let run_end_value = run_end.as_usize() - offset;
369
370 // All the `logical_indices` that are less than current run end index
371 // belongs to current physical index.
372 while ordered_index < indices_len
373 && logical_indices[ordered_indices[ordered_index]].as_usize() < run_end_value
374 {
375 physical_indices[ordered_indices[ordered_index]] = physical_index;
376 ordered_index += 1;
377 }
378 }
379
380 // If there are input values >= run_ends.last_value then we'll not be able to convert
381 // all logical indices to physical indices.
382 if ordered_index < logical_indices.len() {
383 return Err(logical_indices[ordered_indices[ordered_index]]);
384 }
385 Ok(physical_indices)
386 }
387}
388
389#[cfg(test)]
390mod tests {
391 use crate::buffer::RunEndBuffer;
392
393 #[test]
394 fn test_zero_length_slice() {
395 let buffer = RunEndBuffer::new(vec![1_i32, 4_i32].into(), 0, 4);
396 assert_eq!(buffer.get_start_physical_index(), 0);
397 assert_eq!(buffer.get_end_physical_index(), 1);
398 assert_eq!(buffer.get_physical_index(3), 1);
399
400 for offset in 0..4 {
401 let sliced = buffer.slice(offset, 0);
402 assert_eq!(sliced.get_start_physical_index(), 0);
403 assert_eq!(sliced.get_end_physical_index(), 0);
404 }
405
406 let buffer = RunEndBuffer::new(Vec::<i32>::new().into(), 0, 0);
407 assert_eq!(buffer.get_start_physical_index(), 0);
408 assert_eq!(buffer.get_end_physical_index(), 0);
409 }
410
411 #[test]
412 fn test_sliced_values() {
413 // [0, 0, 1, 2, 2, 2]
414 let buffer = RunEndBuffer::new(vec![2i32, 3, 6].into(), 0, 6);
415
416 // Slice: [0, 1, 2, 2] start: 1, len: 4
417 // Logical indices: 1, 2, 3, 4
418 // Original run ends: [2, 3, 6]
419 // Adjusted: [2-1, 3-1, 6-1] capped at 4 -> [1, 2, 4]
420 let sliced = buffer.slice(1, 4);
421 let sliced_values: Vec<i32> = sliced.sliced_values().collect();
422 assert_eq!(sliced_values, &[1, 2, 4]);
423
424 // Slice: [2, 2] start: 4, len: 2
425 // Original run ends: [2, 3, 6]
426 // Slicing at 4 means we only have the last run (physical index 2, which ends at 6)
427 // Adjusted: [6-4] capped at 2 -> [2]
428 let sliced = buffer.slice(4, 2);
429 let sliced_values: Vec<i32> = sliced.sliced_values().collect();
430 assert_eq!(sliced_values, &[2]);
431 }
432}