Skip to content

Commit 4395d38

Browse files
clauderobert3005
authored andcommitted
Make SpareBufferWriter bounds-checked and drop caller-side unsafe
Replace the raw-pointer cursor with a written-count cursor over spare_capacity_mut, and remove copy_slice_unchecked along with the unsafe blocks at the four PiecewiseSequence call sites. The bounds check is one well-predicted branch per slice: benchmarking the gather loop over 16M u64 elements shows checked copies within noise of unchecked ones at every run length (212ms vs 200ms even at the pathological run length of 1, where both beat extend_from_slice's 299ms), so the unchecked variant's extra unsafe surface isn't paying for anything. The writer is now #[must_use], and the remaining unsafety is contained to the copy into the bounds-checked spare-capacity range (the not-yet-stable <[MaybeUninit<T>]>::write_copy_of_slice) and the single set_len in finish, justified by the writer's contiguous-prefix invariant. Signed-off-by: "Robert" <robert@spiraldb.com> Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GUSY34eRKq9du5zQK1FuZq
1 parent 9443f05 commit 4395d38

5 files changed

Lines changed: 31 additions & 41 deletions

File tree

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -203,9 +203,7 @@ where
203203
let mut writer = SpareBufferWriter::new(&mut result, output_len)?;
204204
for &start in starts {
205205
let start = start.as_();
206-
// SAFETY: `computed_len == output_len` proves that all fixed-length slices fit in the
207-
// output buffer.
208-
unsafe { writer.copy_slice_unchecked(&values[start..][..length]) };
206+
writer.copy_slice(&values[start..][..length])?;
209207
}
210208
writer.finish()?;
211209
Ok(result.freeze())

vortex-array/src/arrays/primitive/compute/take/mod.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -312,9 +312,7 @@ where
312312
let mut writer = SpareBufferWriter::new(&mut values, output_len)?;
313313
for &start in starts {
314314
let start = start.as_();
315-
// SAFETY: `computed_len == output_len` proves that all fixed-length slices fit in the
316-
// output buffer.
317-
unsafe { writer.copy_slice_unchecked(&source[start..][..length]) };
315+
writer.copy_slice(&source[start..][..length])?;
318316
}
319317
writer.finish()?;
320318
Ok(values.freeze())

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -485,8 +485,7 @@ where
485485
let offset_range = &offsets[start..][..=length];
486486
let byte_start = offset_range[0].as_();
487487
let byte_end = offset_range[length].as_();
488-
// SAFETY: the first pass computed `output_bytes` from these same byte ranges.
489-
unsafe { writer.copy_slice_unchecked(&data[byte_start..][..byte_end - byte_start]) };
488+
writer.copy_slice(&data[byte_start..][..byte_end - byte_start])?;
490489
}
491490
writer.finish()?;
492491

@@ -566,8 +565,7 @@ where
566565
let offset_range = &offsets[start..][..=length];
567566
let byte_start = offset_range[0].as_();
568567
let byte_end = offset_range[length].as_();
569-
// SAFETY: the first pass computed `output_bytes` from these same byte ranges.
570-
unsafe { writer.copy_slice_unchecked(&data[byte_start..][..byte_end - byte_start]) };
568+
writer.copy_slice(&data[byte_start..][..byte_end - byte_start])?;
571569
}
572570
writer.finish()?;
573571

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,9 +178,7 @@ where
178178
let mut writer = SpareBufferWriter::new(&mut views, output_len)?;
179179
for &start in starts {
180180
let start = start.as_();
181-
// SAFETY: `computed_len == output_len` proves that all fixed-length slices fit in the
182-
// output buffer.
183-
unsafe { writer.copy_slice_unchecked(&source[start..][..length]) };
181+
writer.copy_slice(&source[start..][..length])?;
184182
}
185183
writer.finish()?;
186184
Ok(views.freeze())

vortex-buffer/src/spare_buffer_writer.rs

Lines changed: 26 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,19 @@ use crate::BufferMut;
1212
///
1313
/// This is useful when the caller knows the final output length up front and wants to avoid
1414
/// repeatedly growing the initialized length while copying contiguous source slices.
15+
///
16+
/// All writes are bounds-checked against the declared output length; the check is a single
17+
/// well-predicted branch per slice and is dominated by the copy itself. The unsafety is
18+
/// contained to the copy into the bounds-checked spare-capacity range and the final
19+
/// [`set_len`], which [`finish`] justifies from the writer's invariant that copies initialize
20+
/// a contiguous prefix of the spare capacity.
21+
///
22+
/// [`set_len`]: BufferMut::set_len
23+
/// [`finish`]: SpareBufferWriter::finish
24+
#[must_use = "call `finish` to set the buffer length after writing"]
1525
pub struct SpareBufferWriter<'a, T> {
1626
buffer: &'a mut BufferMut<T>,
17-
next: *mut T,
18-
remaining: usize,
27+
written: usize,
1928
output_len: usize,
2029
}
2130

@@ -35,11 +44,9 @@ impl<'a, T: Copy> SpareBufferWriter<'a, T> {
3544
buffer.capacity()
3645
);
3746

38-
let next = buffer.spare_capacity_mut().as_mut_ptr().cast();
3947
Ok(Self {
4048
buffer,
41-
next,
42-
remaining: output_len,
49+
written: 0,
4350
output_len,
4451
})
4552
}
@@ -48,42 +55,33 @@ impl<'a, T: Copy> SpareBufferWriter<'a, T> {
4855
#[inline]
4956
pub fn copy_slice(&mut self, source: &[T]) -> VortexResult<()> {
5057
vortex_ensure!(
51-
source.len() <= self.remaining,
58+
source.len() <= self.output_len - self.written,
5259
"slice copy length {} exceeds remaining output length {}",
5360
source.len(),
54-
self.remaining
61+
self.output_len - self.written
5562
);
56-
// SAFETY: the check above proves that `source` fits in the remaining output slots.
57-
unsafe { self.copy_slice_unchecked(source) };
58-
Ok(())
59-
}
60-
61-
/// Copies `source` to the next output slots without checking the remaining output length.
62-
///
63-
/// # Safety
64-
///
65-
/// The caller must ensure that `source.len()` does not exceed the remaining output length.
66-
#[inline]
67-
pub unsafe fn copy_slice_unchecked(&mut self, source: &[T]) {
68-
debug_assert!(source.len() <= self.remaining);
69-
// SAFETY: `next` points to the first unwritten slot, the caller guarantees the source fits
70-
// in the remaining capacity, and the mutable buffer borrow prevents reallocation.
63+
let end = self.written + source.len();
64+
let dst = &mut self.buffer.spare_capacity_mut()[self.written..end];
65+
// SAFETY: `MaybeUninit<T>` has the same layout as `T`, `dst` and `source` have equal
66+
// lengths, and `dst` lives in the exclusively borrowed buffer so the two cannot overlap.
67+
// This is `<[MaybeUninit<T>]>::write_copy_of_slice`, which is not yet stable.
7168
unsafe {
72-
ptr::copy_nonoverlapping(source.as_ptr(), self.next, source.len());
73-
self.next = self.next.add(source.len());
69+
ptr::copy_nonoverlapping(source.as_ptr(), dst.as_mut_ptr().cast::<T>(), source.len());
7470
}
75-
self.remaining -= source.len();
71+
self.written = end;
72+
Ok(())
7673
}
7774

7875
/// Sets the target buffer length after exactly `output_len` values have been written.
7976
pub fn finish(self) -> VortexResult<()> {
8077
vortex_ensure!(
81-
self.remaining == 0,
78+
self.written == self.output_len,
8279
"slice copy length {} does not match declared output length {}",
83-
self.output_len - self.remaining,
80+
self.written,
8481
self.output_len
8582
);
86-
// SAFETY: successful calls to the copy methods initialized exactly `output_len` slots.
83+
// SAFETY: `copy_slice` initializes the contiguous prefix `0..written` of the spare
84+
// capacity, and `new` checked that `output_len` does not exceed the capacity.
8785
unsafe {
8886
self.buffer.set_len(self.output_len);
8987
}

0 commit comments

Comments
 (0)