smox 0.3.0

Box that stores small objects inline on stack and bigger objects on the heap using Box, Rc or Arc, with CoW semantic
Documentation
  • Coverage
  • 100%
    22 out of 22 items documented3 out of 17 items with examples
  • Size
  • Source code size: 44.29 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 3.4 MB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 12s Average build duration of successful builds.
  • all releases: 12s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • cehteh

A Box like type that keeps small objects on the stack and can allocate bigger objects as Box, Rc or Arc. DerefMut is copy-on-write when the Smox is backed by Rc or Arc

Rationale

In generic contexts one often does not know how big the objects will be. If they are small enough they can be kept on the stack for better performance. If they are bigger they need to be allocated on the heap. Smox allows to do this in a transparent way.

Example

# #![cfg_attr(feature = "nightly", allow(unused))]
# #[cfg(not(feature = "nightly"))]
# fn main() {
use smox::*;

// For the copy-on-write behavior we need T: Clone
#[derive(Clone)]
struct Generic<T: Clone> {
    value: SmoxRc<T, 16>,
}

impl<T: Clone> Generic<T> {
    fn new(value: T) -> Self {
        Self {
            value: SmoxRc::new_rc(value),
        }
    }
}

// we have the guarantee that Generic<T> will always be at most 16 bytes large.
assert!(std::mem::size_of::<Generic<i32>>() <= 16);
assert!(std::mem::size_of::<Generic<[i32; 100]>>() <= 16);

let mut small = Generic::new(42i32);
*small.value += 1;
assert_eq!(*small.value, 43);

let big = Generic::new([0i32;100]);
// Cheap cloning the underlying Rc
let mut big_clone = big.clone();
// Modifying big_clone will trigger a copy-on-write
big_clone.value[0] = 1;

assert_eq!(big.value[0], 0);
assert_eq!(big_clone.value[0], 1);
# }
# #[cfg(feature = "nightly")]
# fn main() {}

Details

  • In the current implementation, the given SIZE is also the size of a Smox or the size of a box types whatever is bigger. Current stable rust lacks the generic_const_exprs support to constrain the size when heap storage is used to a single pointer. This will change in the future, perhaps with a nightly feature flag.
  • The biggest spaces saving are when you can benefit from the copy-on-write behavior of Rc or Arc. Use either of these when there is a chance that the objects will be cloned.
  • When your Smox should stay as small as possible then try the MINIMAL_SIZE as size limit.

Nightly Support

With the nightly feature enabled, Smox uses generic_const_exprs to optimize away unused in-place storage. When T is larger than SIZE, the in-place storage becomes 0 bytes instead of wasting SIZE bytes. Because of alignement requirements it will still be sized a multiple of a pointer size. When stored on heap it is a single pointer, when stored in_place it is size_of::<T>().next_multiple_of(std::mem::size_of::<usize>().

However, this optimization requires adding where bounds when using Smox with generic type parameters:

# #![cfg_attr(feature = "nightly", feature(generic_const_exprs))]
# #![cfg_attr(not(feature = "nightly"), allow(unused))]
# #[cfg(feature = "nightly")]
# fn main() {
use smox::*;

// On nightly, generic structs need the optimal_size where bound
#[derive(Clone)]
struct Generic<T: Clone>
where
    [(); optimal_size::<T, 32>()]:,
{
    value: SmoxRc<T, 32>,
}

impl<T: Clone> Generic<T>
where
    [(); optimal_size::<T, 32>()]:,
{
    fn new(value: T) -> Self {
        Self {
            value: SmoxRc::new_rc(value),
        }
    }
}

// Size behavior with nightly optimization:
// - When T > SIZE: stored on heap, Smox is pointer-sized (in_place optimized to 0)
// - When T <= SIZE: stored in_place, size is size_of::<T>() rounded up to pointer alignment
//   (because the union contains pointer fields which require pointer alignment)

// i32 (4 bytes) fits in SIZE=32, stored in_place
// Size = 4.next_multiple_of(8) = 8 = pointer size
assert_eq!(std::mem::size_of::<Generic<i32>>(), std::mem::size_of::<usize>());

// [i32; 100] (400 bytes) > SIZE=32, stored on heap → ptr size (in_place is 0!)
// Without nightly optimization, this would be 32 bytes!
assert_eq!(std::mem::size_of::<Generic<[i32; 100]>>(), std::mem::size_of::<usize>());

// [u8; 12] (12 bytes) fits in SIZE=32, stored in_place
// Size = 12.next_multiple_of(8) = 16 (padding added for pointer alignment)
assert_eq!(
    std::mem::size_of::<Generic<[u8; 12]>>(),
    std::mem::size_of::<[u8; 12]>().next_multiple_of(std::mem::size_of::<usize>())
);

let mut small = Generic::new(42i32);
*small.value += 1;
assert_eq!(*small.value, 43);

let big = Generic::new([0i32;100]);
// Cheap cloning the underlying Rc
let mut big_clone = big.clone();
// Modifying big_clone will trigger a copy-on-write
big_clone.value[0] = 1;

assert_eq!(big.value[0], 0);
assert_eq!(big_clone.value[0], 1);
# }
# #[cfg(not(feature = "nightly"))]
# fn main() {}