Skip to content

Commit 0ae5a3f

Browse files
committed
optimize reinterpret casts to avoid heap allocation on dereferences
Also add fast paths for common operations (aligned access, deref u8)
1 parent fd1aa3b commit 0ae5a3f

3 files changed

Lines changed: 301 additions & 138 deletions

File tree

‎docs/src/runtime/reinterpret.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,21 @@ Deleting through a reinterpreted pointer frees the original allocation. That is
105105
how `free` works on a buffer that has been cast around: the pointer is
106106
reinterpreted to bytes and the original allocation is deleted.
107107

108+
### Cost
109+
110+
A cast allocates exactly one small object, the view, which holds a weak
111+
reference to the original allocation, the size of the target type, and a
112+
reference to a stateless table of the byte-level operations for the original's
113+
storage type (a single value, a `Vec`, or a boxed slice). Copying or offsetting
114+
the resulting `Ptr` only bumps a reference count, so a loop over a `malloc`ed
115+
array pays for the cast once, not per access.
116+
117+
Accessing memory through a view does not allocate: the bytes of the accessed
118+
element are staged in a stack buffer (a heap buffer is used only for accesses
119+
larger than 64 bytes). When the original allocation is a `u8` buffer, as it is
120+
for everything that comes from `malloc`, reads and writes copy the bytes
121+
directly instead of serializing the elements one by one.
122+
108123
## Known limitations
109124

110125
Reading a struct through a reinterpreted pointer builds a fresh struct with

‎libcc2rs/src/rc.rs‎

Lines changed: 115 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@ use std::{
1111
rc::{Rc, Weak},
1212
};
1313

14-
use crate::reinterpret::{ByteRepr, OriginalAlloc, SingleOriginalAlloc, SliceOriginalAlloc};
14+
use crate::reinterpret::{ByteRepr, OriginalAlloc, with_scratch};
1515

1616
pub type Value<T> = Rc<RefCell<T>>;
1717

1818
pub(crate) struct ReinterpretedView {
1919
// Pointer to the source of reinterpret
20-
pub(crate) alloc: Rc<dyn OriginalAlloc>,
20+
pub(crate) alloc: OriginalAlloc,
2121
// C++ size of the reinterpreted view
2222
elem_byte_size: usize,
2323
}
@@ -46,7 +46,7 @@ pub enum StrongPtr<T> {
4646
offset: usize,
4747
},
4848
Reinterpreted {
49-
alloc: Rc<dyn OriginalAlloc>,
49+
alloc: OriginalAlloc,
5050
byte_offset: usize,
5151
// Local buffer for deref(). None until first access.
5252
// Read-through: refreshed from alloc on every deref() call.
@@ -66,9 +66,10 @@ impl<T: ByteRepr> StrongPtr<T> {
6666
cell,
6767
} => {
6868
// Read-through: always re-read from the original allocation.
69-
let mut buf = vec![0u8; T::byte_size()];
70-
alloc.read_bytes(*byte_offset, &mut buf);
71-
*cell.borrow_mut() = Some(T::from_bytes(&buf));
69+
with_scratch(T::byte_size(), |buf| {
70+
alloc.read_bytes(*byte_offset, buf);
71+
*cell.borrow_mut() = Some(T::from_bytes(buf));
72+
});
7273
Ref::map(cell.borrow(), |opt| opt.as_ref().unwrap())
7374
}
7475
}
@@ -360,7 +361,7 @@ impl<T> Ptr<T> {
360361
offset: self.offset,
361362
},
362363
PtrKind::Reinterpreted(data) => StrongPtr::Reinterpreted {
363-
alloc: Rc::clone(&data.alloc),
364+
alloc: data.alloc.clone(),
364365
byte_offset: self.offset,
365366
cell: RefCell::new(None),
366367
},
@@ -383,33 +384,50 @@ impl<T> Ptr<T> {
383384
return self_any.downcast_ref::<Ptr<U>>().unwrap().clone();
384385
}
385386

386-
if U::byte_size() == 0 {
387-
panic!("cannot reinterpret_cast to zero-sized type");
387+
match self.original_alloc() {
388+
Some((alloc, byte_offset)) => Ptr::<U>::from_original_alloc(alloc, byte_offset),
389+
None => Ptr::null(),
388390
}
391+
}
389392

390-
let src_byte_off = self.offset.wrapping_mul(T::byte_size());
391-
let (alloc, abs_byte_off): (Rc<dyn OriginalAlloc>, usize) = match &self.kind {
392-
PtrKind::Null => return Ptr::null(),
393+
// The original allocation this pointer refers to, together with the byte
394+
// offset of the pointer into it. None for null pointers.
395+
pub(crate) fn original_alloc(&self) -> Option<(OriginalAlloc, usize)>
396+
where
397+
T: ByteRepr,
398+
{
399+
Some(match &self.kind {
400+
PtrKind::Null => return None,
393401
PtrKind::StackSingle(weak) | PtrKind::HeapSingle(weak) => (
394-
Rc::new(SingleOriginalAlloc { weak: weak.clone() }),
395-
src_byte_off,
402+
OriginalAlloc::single(weak),
403+
self.offset.wrapping_mul(T::byte_size()),
396404
),
397405
PtrKind::StackVec(weak) | PtrKind::HeapVec(weak) => (
398-
Rc::new(SliceOriginalAlloc { weak: weak.clone() }),
399-
src_byte_off,
406+
OriginalAlloc::slice(weak),
407+
self.offset.wrapping_mul(T::byte_size()),
400408
),
401409
PtrKind::StackArray(weak) | PtrKind::HeapArray(weak) => (
402-
Rc::new(SliceOriginalAlloc { weak: weak.clone() }),
403-
src_byte_off,
410+
OriginalAlloc::slice(weak),
411+
self.offset.wrapping_mul(T::byte_size()),
404412
),
405-
PtrKind::Reinterpreted(data) => (Rc::clone(&data.alloc), self.offset),
406-
};
413+
PtrKind::Reinterpreted(data) => (data.alloc.clone(), self.offset),
414+
})
415+
}
407416

417+
// A pointer that views the bytes of `alloc`, starting at `byte_offset`, as
418+
// a sequence of `T`. This is the only allocation of a reinterpret cast.
419+
pub(crate) fn from_original_alloc(alloc: OriginalAlloc, byte_offset: usize) -> Self
420+
where
421+
T: ByteRepr,
422+
{
423+
if T::byte_size() == 0 {
424+
panic!("cannot reinterpret_cast to zero-sized type");
425+
}
408426
Ptr {
409-
offset: abs_byte_off,
427+
offset: byte_offset,
410428
kind: PtrKind::Reinterpreted(Rc::new(ReinterpretedView {
411429
alloc,
412-
elem_byte_size: U::byte_size(),
430+
elem_byte_size: T::byte_size(),
413431
})),
414432
}
415433
}
@@ -438,15 +456,14 @@ impl<T> Ptr<T> {
438456
let mut borrow = rc.borrow_mut();
439457
f(&mut borrow[self.offset])
440458
}
441-
PtrKind::Reinterpreted(data) => {
442-
let mut buf = vec![0u8; T::byte_size()];
443-
data.alloc.read_bytes(self.offset, &mut buf);
444-
let mut val = T::from_bytes(&buf);
459+
PtrKind::Reinterpreted(data) => with_scratch(T::byte_size(), |buf| {
460+
data.alloc.read_bytes(self.offset, buf);
461+
let mut val = T::from_bytes(buf);
445462
let ret = f(&mut val);
446-
val.to_bytes(&mut buf);
447-
data.alloc.write_bytes(self.offset, &buf);
463+
val.to_bytes(buf);
464+
data.alloc.write_bytes(self.offset, buf);
448465
ret
449-
}
466+
}),
450467
}
451468
}
452469

@@ -472,12 +489,10 @@ impl<T> Ptr<T> {
472489
let borrow = rc.borrow();
473490
f(&borrow[self.offset])
474491
}
475-
PtrKind::Reinterpreted(data) => {
476-
let mut buf = vec![0u8; T::byte_size()];
477-
data.alloc.read_bytes(self.offset, &mut buf);
478-
let val = T::from_bytes(&buf);
479-
f(&val)
480-
}
492+
PtrKind::Reinterpreted(data) => with_scratch(T::byte_size(), |buf| {
493+
data.alloc.read_bytes(self.offset, buf);
494+
f(&T::from_bytes(buf))
495+
}),
481496
}
482497
}
483498
}
@@ -503,13 +518,12 @@ impl Ptr<u8> {
503518
let mut b = rc.borrow_mut();
504519
f(&mut b[off..off + len])
505520
}
506-
PtrKind::Reinterpreted(data) => {
507-
let mut buf = vec![0u8; len];
508-
data.alloc.read_bytes(off, &mut buf);
509-
let r = f(&mut buf);
510-
data.alloc.write_bytes(off, &buf);
521+
PtrKind::Reinterpreted(data) => with_scratch(len, |buf| {
522+
data.alloc.read_bytes(off, buf);
523+
let r = f(buf);
524+
data.alloc.write_bytes(off, buf);
511525
r
512-
}
526+
}),
513527
}
514528
}
515529

@@ -533,11 +547,10 @@ impl Ptr<u8> {
533547
let b = rc.borrow();
534548
f(&b[off..off + len])
535549
}
536-
PtrKind::Reinterpreted(data) => {
537-
let mut buf = vec![0u8; len];
538-
data.alloc.read_bytes(off, &mut buf);
539-
f(&buf)
540-
}
550+
PtrKind::Reinterpreted(data) => with_scratch(len, |buf| {
551+
data.alloc.read_bytes(off, buf);
552+
f(buf)
553+
}),
541554
}
542555
}
543556

@@ -999,15 +1012,17 @@ impl<T: 'static> ByteRepr for Ptr<T> {}
9991012

10001013
impl<T: 'static> Ptr<T> {
10011014
pub fn to_int(&self) -> usize {
1002-
let mut buf = vec![0u8; Self::byte_size()];
1003-
self.to_bytes(&mut buf);
1004-
usize::from_bytes(&buf[..std::mem::size_of::<usize>()])
1015+
with_scratch(Self::byte_size(), |buf| {
1016+
self.to_bytes(buf);
1017+
usize::from_bytes(&buf[..std::mem::size_of::<usize>()])
1018+
})
10051019
}
10061020

10071021
pub fn from_int(value: usize) -> Self {
1008-
let mut buf = vec![0u8; Self::byte_size()];
1009-
value.to_bytes(&mut buf[..std::mem::size_of::<usize>()]);
1010-
Self::from_bytes(&buf)
1022+
with_scratch(Self::byte_size(), |buf| {
1023+
value.to_bytes(&mut buf[..std::mem::size_of::<usize>()]);
1024+
Self::from_bytes(buf)
1025+
})
10111026
}
10121027
}
10131028

@@ -1047,6 +1062,54 @@ mod tests {
10471062
p.decay().delete();
10481063
}
10491064

1065+
#[test]
1066+
fn reinterpreted_unaligned_access_spans_elements() {
1067+
let p: Ptr<u16> = Ptr::alloc_array(vec![0u16; 4].into_boxed_slice());
1068+
let words = p
1069+
.reinterpret_cast::<u8>()
1070+
.offset(2)
1071+
.reinterpret_cast::<u32>();
1072+
words.write(0xAABBCCDD);
1073+
assert_eq!(words.read(), 0xAABBCCDD);
1074+
let halves: Vec<u16> = (0..4).map(|i| p.offset(i).read()).collect();
1075+
assert_eq!(halves, vec![0, 0xCCDD, 0xAABB, 0]);
1076+
p.delete();
1077+
}
1078+
1079+
#[test]
1080+
fn reinterpreted_u8_storage_is_read_and_written_bytewise() {
1081+
let p: Ptr<u8> = Ptr::alloc_array(vec![0u8; 16].into_boxed_slice());
1082+
let ints = p.reinterpret_cast::<u32>();
1083+
for i in 0..4 {
1084+
ints.offset(i).write(0x01010101 * (i as u32 + 1));
1085+
}
1086+
assert_eq!(p.offset(5).read(), 0x02);
1087+
assert_eq!(ints.offset(3).read(), 0x04040404);
1088+
// An unaligned view over the same bytes.
1089+
let unaligned = p.offset(1).reinterpret_cast::<u32>();
1090+
assert_eq!(unaligned.read(), 0x02010101);
1091+
p.delete();
1092+
}
1093+
1094+
#[test]
1095+
#[should_panic]
1096+
fn reinterpreted_write_past_the_end_panics() {
1097+
let p: Ptr<u32> = Ptr::alloc_array(vec![0u32; 2].into_boxed_slice());
1098+
// A u32 straddling the end of the allocation.
1099+
let q = p
1100+
.reinterpret_cast::<u8>()
1101+
.offset(6)
1102+
.reinterpret_cast::<u32>();
1103+
q.write(1);
1104+
}
1105+
1106+
#[test]
1107+
#[should_panic]
1108+
fn reinterpreted_u8_write_past_the_end_panics() {
1109+
let p: Ptr<u8> = Ptr::alloc_array(vec![0u8; 6].into_boxed_slice());
1110+
p.reinterpret_cast::<u32>().offset(1).write(1);
1111+
}
1112+
10501113
#[test]
10511114
fn reinterpreted_cast() {
10521115
let p: Ptr<u64> = Ptr::alloc(0x0807060504030201u64);

0 commit comments

Comments
 (0)