Skip to content

Commit 85aad72

Browse files
authored
Optimize buffer ops (#8322)
## Summary This PR includes a few optimization for buffer-level ops: 1. Empty buffer allocations are don't allocate anymore, using a shared static ptr. 2. Moved some alignment checks to direct comparison or mask-based checks instead of division. 3. More of a refactoring - but more of the alignment related checks are now functions on `Alignment`, instead of having less specific checks in different callsites. After this PR is merged, I'll follow up and try to remove `bitvec` as a dependency, its currently used in a couple of pretty random places and I suspect there's nothing special about them compared to our own `BitBuffer`. --------- Signed-off-by: Adam Gutglick <adam@spiraldb.com>
1 parent e0ac2bd commit 85aad72

6 files changed

Lines changed: 186 additions & 23 deletions

File tree

vortex-buffer/src/alignment.rs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ impl Alignment {
5454
Self::new(align_of::<T>())
5555
}
5656

57+
/// The largest valid alignment: the greatest power of 2 that fits into a `u16`.
58+
pub const MAX: Alignment = Alignment::new(1 << 15);
59+
5760
/// Check if `self` alignment is a "larger" than `other` alignment.
5861
///
5962
/// ## Example
@@ -67,10 +70,32 @@ impl Alignment {
6770
/// assert!(!b.is_aligned_to(a));
6871
/// ```
6972
#[inline]
70-
pub fn is_aligned_to(&self, other: Alignment) -> bool {
71-
// Since we know alignments are powers of 2, we can compare them by checking if the number
72-
// of trailing zeros in the binary representation of the alignment is greater or equal.
73-
self.0.trailing_zeros() >= other.0.trailing_zeros()
73+
pub const fn is_aligned_to(&self, other: Alignment) -> bool {
74+
// Since both alignments are powers of 2, divisibility is equivalent to ordering.
75+
self.0 >= other.0
76+
}
77+
78+
/// Check if the given byte offset (or length) is a multiple of this alignment.
79+
///
80+
/// ## Example
81+
///
82+
/// ```
83+
/// use vortex_buffer::Alignment;
84+
///
85+
/// let a = Alignment::new(4);
86+
/// assert!(a.is_offset_aligned(8));
87+
/// assert!(!a.is_offset_aligned(2));
88+
/// ```
89+
#[inline]
90+
pub const fn is_offset_aligned(&self, offset: usize) -> bool {
91+
// Alignment is always a power of 2, so a mask test is equivalent to `offset % self == 0`.
92+
offset & (self.0 - 1) == 0
93+
}
94+
95+
/// Check if the given pointer is aligned to this alignment.
96+
#[inline]
97+
pub fn is_ptr_aligned<T>(&self, ptr: *const T) -> bool {
98+
self.is_offset_aligned(ptr.addr())
7499
}
75100

76101
/// Returns the log2 of the alignment.

vortex-buffer/src/bit/buf.rs

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -228,10 +228,11 @@ impl BitBuffer {
228228
unsafe { buffer.push_unchecked(packed) }
229229
}
230230

231-
buffer.truncate(len.div_ceil(8));
231+
let mut bytes = buffer.into_byte_buffer();
232+
bytes.truncate(len.div_ceil(8));
232233

233234
Self {
234-
buffer: buffer.freeze().into_byte_buffer(),
235+
buffer: bytes.freeze(),
235236
offset: 0,
236237
len,
237238
}
@@ -312,7 +313,23 @@ impl BitBuffer {
312313
assert!(end <= self.len);
313314
let len = end - start;
314315

315-
Self::new_with_offset(self.buffer.clone(), len, self.offset + start)
316+
let offset = self.offset + start;
317+
let byte_offset = offset / 8;
318+
let bit_offset = offset % 8;
319+
320+
// Trim whole bytes off the front directly rather than going through `new_with_offset`,
321+
// which would slice (and re-clone) the clone we'd have to pass it.
322+
let buffer = if byte_offset != 0 {
323+
self.buffer.slice_unaligned(byte_offset..)
324+
} else {
325+
self.buffer.clone().aligned(Alignment::none())
326+
};
327+
328+
Self {
329+
buffer,
330+
offset: bit_offset,
331+
len,
332+
}
316333
}
317334

318335
/// Slice any full bytes from the buffer, leaving the offset < 8.

vortex-buffer/src/bit/buf_mut.rs

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,9 @@ impl BitBufferMut {
267267

268268
/// Clears the bit buffer (but keeps any allocated memory).
269269
pub fn clear(&mut self) {
270-
// Since there are no items we need to drop, we simply set the length to 0.
270+
// Also clear the byte buffer (not just `len`) so the "bits beyond len are zero"
271+
// invariant holds; `append_false` and `append_buffer` rely on it.
272+
self.buffer.clear();
271273
self.len = 0;
272274
self.offset = 0;
273275
}
@@ -649,6 +651,8 @@ impl FromIterator<bool> for BitBufferMut {
649651

650652
#[cfg(test)]
651653
mod tests {
654+
use rstest::rstest;
655+
652656
use crate::BufferMut;
653657
use crate::bit::buf_mut::BitBufferMut;
654658
use crate::bitbuffer;
@@ -923,6 +927,65 @@ mod tests {
923927
assert!(frozen.value(7));
924928
}
925929

930+
#[cfg_attr(miri, ignore)] // bitvec crate uses a ptr cast that Miri doesn't support
931+
#[test]
932+
fn append_after_clear_reads_back_false() {
933+
// `clear` must not leave stale set bits behind: `append_false` and `append_buffer`
934+
// rely on bits beyond `len` being zero.
935+
let mut bools = BitBufferMut::new_set(16);
936+
bools.clear();
937+
bools.append_false();
938+
bools.append_buffer(&crate::BitBuffer::new_unset(8));
939+
940+
let bools = bools.freeze();
941+
assert_eq!(bools.len(), 9);
942+
assert_eq!(bools.true_count(), 0);
943+
}
944+
945+
#[cfg_attr(miri, ignore)] // bitvec crate uses a ptr cast that Miri doesn't support
946+
#[test]
947+
fn test_append_buffer_after_truncate() {
948+
// Truncating leaves stale set bits in the last partial byte; an append after that
949+
// must overwrite them rather than OR into them.
950+
let mut buf = BitBufferMut::new_set(16);
951+
buf.truncate(3);
952+
buf.append_buffer(&crate::BitBuffer::new_unset(8));
953+
954+
let frozen = buf.freeze();
955+
assert_eq!(frozen.len(), 11);
956+
for i in 0..3 {
957+
assert!(frozen.value(i), "bit {i} should be set");
958+
}
959+
for i in 3..11 {
960+
assert!(!frozen.value(i), "bit {i} should be unset");
961+
}
962+
}
963+
964+
#[rstest]
965+
#[case::both_aligned(0, 0)]
966+
#[case::dst_unaligned(3, 0)]
967+
#[case::src_unaligned(0, 5)]
968+
#[case::mismatched(3, 5)]
969+
#[case::equal_nonzero(5, 5)]
970+
#[cfg_attr(miri, ignore)] // bitvec crate uses a ptr cast that Miri doesn't support
971+
fn test_append_buffer_long(#[case] dst_prefix: usize, #[case] src_start: usize) {
972+
// Exercise every alignment combination across many words.
973+
let source = crate::BitBuffer::from_iter((0..301).map(|i| i % 3 == 0));
974+
let source = source.slice(src_start..301);
975+
976+
let mut dest = BitBufferMut::with_capacity(512);
977+
dest.append_n(true, dst_prefix);
978+
dest.append_buffer(&source);
979+
980+
assert_eq!(dest.len(), dst_prefix + source.len());
981+
for i in 0..dst_prefix {
982+
assert!(dest.value(i), "prefix bit {i}");
983+
}
984+
for i in 0..source.len() {
985+
assert_eq!(dest.value(dst_prefix + i), source.value(i), "bit {i}");
986+
}
987+
}
988+
926989
#[cfg_attr(miri, ignore)] // bitvec crate uses a ptr cast that Miri doesn't support
927990
#[test]
928991
fn test_append_buffer_with_offsets() {

vortex-buffer/src/buffer.rs

Lines changed: 69 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,20 @@ pub struct Buffer<T> {
3232
pub(crate) _marker: PhantomData<T>,
3333
}
3434

35+
/// Zero-length backing for empty buffers, "aligned" to [`Alignment::MAX`] so it satisfies any
36+
/// valid alignment without allocating. A zero-length slice never reads memory, so it may use a
37+
/// dangling pointer as long as it is non-null and aligned.
38+
const EMPTY_BACKING: &[u8] = {
39+
let addr = 1usize << 20;
40+
assert!(Alignment::MAX.is_offset_aligned(addr));
41+
// SAFETY: the pointer is non-null and aligned, and the slice is zero-length.
42+
unsafe { std::slice::from_raw_parts(std::ptr::without_provenance(addr), 0) }
43+
};
44+
3545
impl<T> Default for Buffer<T> {
3646
fn default() -> Self {
3747
Self {
38-
bytes: Default::default(),
48+
bytes: Bytes::from_static(EMPTY_BACKING),
3949
length: 0,
4050
alignment: Alignment::of::<T>(),
4151
_marker: PhantomData,
@@ -101,12 +111,27 @@ impl<T> Buffer<T> {
101111

102112
/// Create a new empty `ByteBuffer` with the provided alignment.
103113
pub fn empty() -> Self {
104-
BufferMut::empty().freeze()
114+
Self::empty_aligned(Alignment::of::<T>())
105115
}
106116

107117
/// Create a new empty `ByteBuffer` with the provided alignment.
118+
///
119+
/// This does not allocate: empty buffers are backed by a zero-length `Bytes` that is
120+
/// aligned to [`Alignment::MAX`].
108121
pub fn empty_aligned(alignment: Alignment) -> Self {
109-
BufferMut::empty_aligned(alignment).freeze()
122+
if !alignment.is_aligned_to(Alignment::of::<T>()) {
123+
vortex_panic!(
124+
"Alignment {} must align to the scalar type's alignment {}",
125+
alignment,
126+
Alignment::of::<T>(),
127+
);
128+
}
129+
Self {
130+
bytes: Bytes::from_static(EMPTY_BACKING),
131+
length: 0,
132+
alignment,
133+
_marker: PhantomData,
134+
}
110135
}
111136

112137
/// Create a new full `ByteBuffer` with the given value.
@@ -152,7 +177,7 @@ impl<T> Buffer<T> {
152177
Alignment::of::<T>(),
153178
);
154179
}
155-
if bytes.as_ptr().align_offset(*alignment) != 0 {
180+
if !alignment.is_ptr_aligned(bytes.as_ptr()) {
156181
vortex_panic!(
157182
"Bytes alignment must align to the requested alignment {}",
158183
alignment,
@@ -320,7 +345,7 @@ impl<T> Buffer<T> {
320345
let begin_byte = begin * size_of::<T>();
321346
let end_byte = end * size_of::<T>();
322347

323-
if !begin_byte.is_multiple_of(*alignment) {
348+
if !alignment.is_offset_aligned(begin_byte) {
324349
vortex_panic!(
325350
"range start must be aligned to {alignment:?}, byte {}",
326351
begin_byte
@@ -369,7 +394,7 @@ impl<T> Buffer<T> {
369394
vortex_panic!("slice_ref subset alignment must at least align to the buffer alignment")
370395
}
371396

372-
if subset.as_ptr().align_offset(*alignment) != 0 {
397+
if !alignment.is_ptr_aligned(subset.as_ptr()) {
373398
vortex_panic!("slice_ref subset must be aligned to {:?}", alignment);
374399
}
375400

@@ -435,17 +460,17 @@ impl<T> Buffer<T> {
435460
/// Convert self into `BufferMut<T>`, cloning the data if there are multiple strong references.
436461
pub fn into_mut(self) -> BufferMut<T> {
437462
self.try_into_mut()
438-
.unwrap_or_else(|buffer| BufferMut::<T>::copy_from(&buffer))
463+
.unwrap_or_else(|buffer| BufferMut::<T>::copy_from_aligned(&buffer, buffer.alignment))
439464
}
440465

441466
/// Returns whether a `Buffer<T>` is aligned to the given alignment.
442467
pub fn is_aligned(&self, alignment: Alignment) -> bool {
443-
self.bytes.as_ptr().align_offset(*alignment) == 0
468+
alignment.is_ptr_aligned(self.bytes.as_ptr())
444469
}
445470

446471
/// Return a `Buffer<T>` with the given alignment. Where possible, this will be zero-copy.
447472
pub fn aligned(mut self, alignment: Alignment) -> Self {
448-
if self.as_ptr().align_offset(*alignment) == 0 {
473+
if alignment.is_ptr_aligned(self.as_ptr()) {
449474
self.alignment = alignment;
450475
self
451476
} else {
@@ -462,7 +487,7 @@ impl<T> Buffer<T> {
462487

463488
/// Return a `Buffer<T>` with the given alignment. Panics if the buffer is not aligned.
464489
pub fn ensure_aligned(mut self, alignment: Alignment) -> Self {
465-
if self.as_ptr().align_offset(*alignment) == 0 {
490+
if alignment.is_ptr_aligned(self.as_ptr()) {
466491
self.alignment = alignment;
467492
self
468493
} else {
@@ -634,7 +659,7 @@ impl Buf for ByteBuffer {
634659

635660
#[inline]
636661
fn advance(&mut self, cnt: usize) {
637-
if !cnt.is_multiple_of(*self.alignment) {
662+
if !self.alignment.is_offset_aligned(cnt) {
638663
vortex_panic!(
639664
"Cannot advance buffer by {} items, resulting alignment is not {}",
640665
cnt,
@@ -786,6 +811,31 @@ mod test {
786811
assert_eq!(vec, buff.as_ref());
787812
}
788813

814+
#[test]
815+
fn empty_aligned_max_alignment() {
816+
// Empty buffers are backed by a static and must satisfy any valid alignment.
817+
let buf = Buffer::<u8>::empty_aligned(Alignment::MAX);
818+
assert!(buf.is_empty());
819+
assert!(buf.is_aligned(Alignment::MAX));
820+
}
821+
822+
#[test]
823+
fn empty_slice_preserves_alignment() {
824+
let buf = Buffer::<u64>::zeroed_aligned(8, Alignment::new(64));
825+
let sliced = buf.slice(0..0);
826+
assert!(sliced.is_empty());
827+
assert_eq!(sliced.alignment(), Alignment::new(64));
828+
assert!(sliced.is_aligned(Alignment::new(64)));
829+
}
830+
831+
#[test]
832+
fn empty_into_mut_preserves_alignment() {
833+
let buf = Buffer::<u8>::empty_aligned(Alignment::new(64));
834+
let buf_mut = buf.into_mut();
835+
assert_eq!(buf_mut.alignment(), Alignment::new(64));
836+
assert!(buf_mut.is_empty());
837+
}
838+
789839
#[test]
790840
fn test_slice_unaligned_end_pos() {
791841
let data = vec![0u8; 2];
@@ -797,4 +847,12 @@ mod test {
797847
// to be aligned.
798848
aligned_buffer.slice(0..1);
799849
}
850+
851+
#[test]
852+
fn test_empty_equality() {
853+
let a = Buffer::<u16>::empty();
854+
let b = Buffer::<u16>::empty();
855+
856+
assert_eq!(a, b);
857+
}
800858
}

vortex-buffer/src/buffer_mut.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,7 @@ impl<T> BufferMut<T> {
358358
}
359359

360360
let bytes_at = at * size_of::<T>();
361-
if !bytes_at.is_multiple_of(*self.alignment) {
361+
if !self.alignment.is_offset_aligned(bytes_at) {
362362
vortex_panic!(
363363
"Cannot split buffer at {}, resulting alignment is not {}",
364364
at,
@@ -742,7 +742,7 @@ impl Buf for ByteBufferMut {
742742
}
743743

744744
fn advance(&mut self, cnt: usize) {
745-
if !cnt.is_multiple_of(*self.alignment) {
745+
if !self.alignment.is_offset_aligned(cnt) {
746746
vortex_panic!(
747747
"Cannot advance buffer by {} items, resulting alignment is not {}",
748748
cnt,
@@ -765,7 +765,7 @@ unsafe impl BufMut for ByteBufferMut {
765765

766766
#[inline]
767767
unsafe fn advance_mut(&mut self, cnt: usize) {
768-
if !cnt.is_multiple_of(*self.alignment) {
768+
if !self.alignment.is_offset_aligned(cnt) {
769769
vortex_panic!(
770770
"Cannot advance buffer by {} items, resulting alignment is not {}",
771771
cnt,

vortex-buffer/src/string.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ impl BufferString {
2525

2626
/// Creates an empty `BufferString`.
2727
pub fn empty() -> Self {
28-
Self(ByteBuffer::from(vec![]))
28+
Self(ByteBuffer::empty())
2929
}
3030

3131
/// Return a view of the contents of BufferString as an immutable `&str`.

0 commit comments

Comments
 (0)