Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vec: remove code duplication due to VecView. #486

Merged
merged 4 commits into from
Jun 30, 2024

Conversation

Dirbaio
Copy link
Member

@Dirbaio Dirbaio commented Jun 27, 2024

I'm a bit concerned about the code duplication introduced by #425. Most Vec
methods forward to their VecView counterparts, but the function signature
and the doc comments are still duplicated. This means every time we do a doc
fix or add a new method we have to remember to do it on both sides.

It has worked fine for for experimenting and proving the concept. However,
now we're at a stage where it's proven and we have to commit to
add it to all the other containers: #452 #459 #479 #480 #481 #485.
Each of these PRs adds 400-600 lines to the codebase, mostly duplicated
signatures and docs.

IMO we should find a way of adding View types without so much duplication
first, then add it to all containers.

This PR implements a possible solution I've found that removes all the duplication:

  • Added a Storage trait with two impls, for sized and unsized.
  • Vec is now generic over Storage instead of over the buffer type. The buffer type is now a Buffer associated type.
  • Storage is sealed. The Buffer associated type is private. The user cannot learn the Buffer type for the
    Storage impls, they can just use it with VecInner and it makes it behave differently magically.
  • Most methods are implemented for VecInner<S> for any S: Storage. This makes them
    available on both Vec and VecView without duplicating anything.

Advantages:

  • No duplication.
  • No macros.
  • Trait impls are generic over Storage now, so you can e.g. compare a Vec with a VecView. Before you couldn't, you could only do Vec-Vec or VecView-VecView.
  • Users can now write code or trait impls generic over the storage.

Docs work fine on latest stable, it doesn't seem to need any workarounds:

  • docs for Vec show the Vec-only methods and the storage-independent methods.
  • docs for VecView show the VecView-only methods (if we had some, we don't) and the storage-independent methods.
  • docs for Vecinner show all methods.

I also propose making mod vec public, including Storage, VecInner. The reasons are:

  • vec::IntoIter was already in the public API, but was previously unnameable.
  • Exposing Storage, VecInner can be useful for users that want to write code or trait impls generic over the storage.
  • Now that Storage is a sealed trait and the Buffer type is hidden, it's not leaking implementation details anymore. There's no [MaybeUninit<T>] in the public API, we can change it at any time without it being a breaking change.

I think we also should remove the Vec, VecView reexports in the crate root, but this is left for a future PR.

cc @sosthene-nitrokey

@Dirbaio
Copy link
Member Author

Dirbaio commented Jun 28, 2024

Tested code size on company firmware:

   text    data     bss     dec     hex filename
 378136    1796  182436  562368   894c0 heapless git v0.8.0
 378168    1796  182436  562400   894e0 heapless git main
 378232    1796  182436  562464   89520 heapless git vecview-dedup

slight 0.02% bloat, but imo it's within the margin of error.

@sosthene-nitrokey
Copy link
Contributor

I thought about this approach initially but didn't go for it because:

  • had concerns about the documentation generated, but there were bugs with Rust doc that have been fixed since then, so it may be actually better now
  • The initial reasoning behind this PR was to reduce binary size by reducing the need for monomorphization for each value N. I'll re-run the comparison again but if I remember correctly this lead to 2% size reduction in our firmware, i'm testing this again.
  • Users can now write code or trait impls generic over the storage.

I'm not sure I see the benefits of that. My idea was that we would end up using VecView pretty much everywhere we can, and when we can't it would be because it's !Sized, and thus not

On the other hand, I agree with the benefits of this approach:

@sosthene-nitrokey
Copy link
Contributor

Ok, on our firmware (we have 4 versions) we see the following sizes (in bytes):

Current main

1397551
2007561
501460
576036

Heapless 0.8

1398803
2014626
502048
576516

This PR

1397543
2010261
501312
575940

The biggest difference we can see is from heapless 0.8 to heapless main, with a difference of 0.3% smaller firmware size. The difference is lower that what I measured previously, but I'm not sure why (I tested with heapless 0.7 at the time).

src/vec.rs Outdated
Comment on lines 15 to 41
/// Trait defining how the data for a Vec is stored.
///
/// There's two implementations available:
///
/// - [`VecStorage`]: the storage for a [`Vec`]. Stores the data in an array `[MaybeUninit<T>; N]` whose size is known at compile time.
/// - [`VecViewStorage`]: the storage for a [`VecView`]. Stores the data in an unsized `[MaybeUninit<T>]`.
///
/// This allows [`VecInner`] (the base struct for vectors) to be generic over either sized
/// or unsized storage.
///
/// This trait is sealed, so you cannot implement it for your own types. You can only use
/// the implementations provided by this crate.
#[allow(private_bounds)]
pub trait Storage: SealedStorage {}

/// Implementation of [`Storage`] for [`Vec`].
pub enum VecStorage<const N: usize> {}
impl<const N: usize> Storage for VecStorage<N> {}
impl<const N: usize> SealedStorage for VecStorage<N> {
type Buffer<T> = [MaybeUninit<T>; N];
}

impl<T> VecBuffer for [MaybeUninit<T>] {
type T = T;

fn as_vecview(vec: &VecInner<Self>) -> &VecView<Self::T> {
vec
}
fn as_mut_vecview(vec: &mut VecInner<Self>) -> &mut VecView<Self::T> {
vec
}
/// Implementation of [`Storage`] for [`VecView`].
pub enum VecViewStorage {}
impl Storage for VecViewStorage {}
impl SealedStorage for VecViewStorage {
type Buffer<T> = [MaybeUninit<T>];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think It would make sense to for these structs be moved in a common module for the crate, and rename the structs to Owned and View, so that the same structs can be used for other containers ?

I think it's clearer to use the same structs for all containers.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point, done!

@sosthene-nitrokey
Copy link
Contributor

I would instead use:

trait SealedStorage {
    type Buffer<T>: ?Sized + Borrow<[T]> + BorrowMut<[T]>;
}

And have VecInner be defined to be:

pub struct VecInner<T, S: Storage> 
    len: usize,
    buffer: S::Buffer<MaybeUninit<T>>,
}

That would make the Storage more generic, and make it usable for all container structures. For example mpmc would be defined as:

pub struct MpMcQueueInner<S: Storage> {
    pub(super) dequeue_pos: AtomicTargetSize,
    pub(super) enqueue_pos: AtomicTargetSize,
    pub(super) buffer: UnsafeCell<S::Buffer<Cell<T>>>,
}

Which can't be done if the Buffer always introduces a MaybeUninit.

@Dirbaio
Copy link
Member Author

Dirbaio commented Jun 28, 2024

I'm not sure I see the benefits of that. My idea was that we would end up using VecView pretty much everywhere we can, and when we can't it would be because it's !Sized, and thus not

yeah it's a rather weak advantage, but I can't see why not allow it.

I would instead use: ... type Buffer<T>: ?Sized + Borrow<[T]> + BorrowMut<[T]>

very true, not all containers use MaybeUninit. Changed!

src/storage.rs Outdated
Comment on lines 9 to 21
/// Trait defining how data for a container is stored.
///
/// There's two implementations available:
///
/// - [`OwnedStorage`]: stores the data in an array `[T; N]` whose size is known at compile time.
/// - [`ViewStorage`]: stores the data in an unsized `[T]`.
///
/// This allows containers to be generic over either sized or unsized storage.
///
/// This trait is sealed, so you cannot implement it for your own types. You can only use
/// the implementations provided by this crate.
#[allow(private_bounds)]
pub trait Storage: SealedStorage {}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would add an intra-doc link to the documentation of VecView and Vec::as_view to explain what these traits are used for.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done!

Copy link
Contributor

@sosthene-nitrokey sosthene-nitrokey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is indeed much cleaner for maintenance than the current duplicated documentation.
Since the size benefits are minor, I'm all for this PR.

I just wish it were possible to have the examples in the documentation of VecView to actually use VecView and not Vec but well, we can live with that.

Copy link
Contributor

@sosthene-nitrokey sosthene-nitrokey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation for Serialize for Vec should be over VecInner and generic over the Storage

@sosthene-nitrokey
Copy link
Contributor

Using Storage for other implementations becomes so efficient with this !

For example Stringc053490

@Dirbaio
Copy link
Member Author

Dirbaio commented Jun 28, 2024

The implementation for Serialize for Vec should be over VecInner and generic over the Storage

done. I was also missing defmt, ufmt, done that too.

@Dirbaio Dirbaio added this pull request to the merge queue Jun 30, 2024
Merged via the queue into rust-embedded:main with commit d1c47c3 Jun 30, 2024
22 checks passed
@Dirbaio Dirbaio deleted the vecview-dedup branch June 30, 2024 21:50
sosthene-nitrokey added a commit to Nitrokey/heapless that referenced this pull request Jul 1, 2024
sosthene-nitrokey added a commit to Nitrokey/heapless that referenced this pull request Jul 1, 2024
github-merge-queue bot pushed a commit that referenced this pull request Jul 1, 2024
String: implement `StringView` on top of #486
sosthene-nitrokey added a commit to Nitrokey/heapless that referenced this pull request Jul 1, 2024
github-merge-queue bot pushed a commit that referenced this pull request Jul 1, 2024
 `MpMcQueue`: add `MpMcQueueView`, similar to `VecView` on top of #486
sosthene-nitrokey added a commit to Nitrokey/heapless that referenced this pull request Jul 1, 2024
github-merge-queue bot pushed a commit that referenced this pull request Jul 1, 2024
LinearMap: add LinearMapView, similar to VecView on top of #486
sosthene-nitrokey added a commit to Nitrokey/heapless that referenced this pull request Jul 1, 2024
sosthene-nitrokey added a commit to Nitrokey/heapless that referenced this pull request Jul 1, 2024
sosthene-nitrokey added a commit to Nitrokey/heapless that referenced this pull request Jul 1, 2024
sosthene-nitrokey added a commit to Nitrokey/heapless that referenced this pull request Jul 1, 2024
sosthene-nitrokey added a commit to Nitrokey/heapless that referenced this pull request Jul 1, 2024
github-merge-queue bot pushed a commit that referenced this pull request Jul 1, 2024
sosthene-nitrokey added a commit to Nitrokey/heapless that referenced this pull request Jul 1, 2024
github-merge-queue bot pushed a commit that referenced this pull request Jul 1, 2024
BinaryHeap: implement BinaryHeapView on top of #486
sosthene-nitrokey added a commit to Nitrokey/heapless that referenced this pull request Jul 1, 2024
github-merge-queue bot pushed a commit that referenced this pull request Jul 1, 2024
sosthene-nitrokey added a commit to Nitrokey/heapless that referenced this pull request Jul 1, 2024
sosthene-nitrokey added a commit to Nitrokey/heapless that referenced this pull request Jul 1, 2024
github-merge-queue bot pushed a commit that referenced this pull request Jul 1, 2024
Queue: implement QueueView on top of #486
github-merge-queue bot pushed a commit that referenced this pull request Jul 1, 2024
 SortedLinkedList: add SortedLinkedListView, similar to VecView on top of #486
sosthene-nitrokey added a commit to Nitrokey/heapless that referenced this pull request Jul 2, 2024
In 0.8.0 it was const, but this was removed in rust-embedded#486

The other container types did not make the `capacity` method const, and therefore can kept with the normal name and the generic implementation.
sosthene-nitrokey added a commit to Nitrokey/heapless that referenced this pull request Jul 2, 2024
In 0.8.0 it was const, but this was removed in rust-embedded#486

The other container types did not make the `capacity` method const, and therefore can kept with the normal name and the generic implementation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants