# Is custom allocators the right abstraction?

**URL:** https://internals.rust-lang.org/t/is-custom-allocators-the-right-abstraction/13460
**Category:** libs
**Created:** [November 28, 2020, 4:14pm UTC](https://internals.rust-lang.org/t/is-custom-allocators-the-right-abstraction/13460 "2020-11-28T16:14:15Z")
**Posts on this page:** 12
**Page:** 5

<div class="post-metadata">

### Author: ![matthieum](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/matthieum/32/4023_2.png) [@matthieum](https://internals.rust-lang.org/u/matthieum)
#### Post date: [February 23, 2021, 10:29am UTC](https://internals.rust-lang.org/t/is-custom-allocators-the-right-abstraction/13460/83 "2021-02-23T10:29:33Z")

</div>

_Preface: I renamed `acquire`/`release` to `allocate`/`deallocate`, to be closer to the Allocator API._

* * *

@CAD97 I tried adding support for converting from `Box<T, StorageA>` to `Box<T, StorageB>`, and given that `T` can be `!Sized` at that point, this required me to add an `allocate` method to `ElementStorage` that only takes the meta-data, not the `T`, so you were spot on regarding this comment.

With regard to the exact hierarchy of traits; I am not sure.

At some point in the discussion I was afraid that each data-structure would require a unique trait API. I'm very happy that I managed to distill down the requirements to end up with the 2x2 matrix (Single vs Multi and Element vs Range). It's entirely possible that further simplification is still available... but I am not sure if it's possible, or even desirable:

- I don't think that Single vs Multi should be erased:
  - There's a strong semantic difference since Single doesn't keep track of whether it's "occupied" or not. I am slightly uncomfortable smoothing it out.
  - This difference has repercussions on the implementation: Multi requires extra tracking which is just overhead for Single, so a given storage is generally specialized for one or the other anyway -- the only exception being the allocators.

- Unification of Element vs Range is even more complicated. Differences are:
  - `T: ?Sized + Pointee` vs `T`: yet, if considering the range as a single Element, this should work.
  - `MaybeUninit`: in the case of Range storage. If we change the signature of `resolve` (gonna steal that name...) to going from `Handle<T>` to `NonNull<MaybeUninit<T>>`, then it would be smoothed.
  - `type Capacity`. This latter is critical, it's how a `Vec<u8, inline::SingleRangeStorage<u8, [u8; 31]>>` can take only 32 bytes. At the same time, there's no `Capacity` for Element Storage; it's meaningless.

I can see building a hierarchy like:

- `Storage`: `Handle<T>`, `deallocate`, and `resolve`.
  - `ElementStorage`: `destroy` convenience method.
    - `SingleElementStorage`: `allocate`, and `create` convenience method.
    - `MultiElementStorage`: `allocate`, and `create` convenience method.

  - `RangeStorage`: `Capacity`, `try_grow`, and `try_shrink`.
    - `SingleRangeStorage`: `allocate`.
    - `MultiRangeStorage`: `allocate`.

However I find the `Storage` trait rather... pointless, on its own? I don't have any usecase that would require it right now, though at a guess `resolve` may be useful on its own?

Imagining that we paper over the difference between `Single` and `Multi`, as uncomfortable as this makes me:

- `Storage`: `Handle<T>`, `deallocate`, and `resolve`.
  - `ElementStorage`: `allocate`, and for convenience `create` and `destroy`.
  - `RangeStorage`: `Capacity`, `allocate`, `try_grow`, and `try_shrink`.

And imagining that we're okay asking the user to synthetize a `SliceMeta<T>` out of thin air just to call `allocate`:

- `Storage`: `Handle<T>`, `allocate`, `deallocate`, and `resolve`.
  - `ElementStorage`: convenience `create` and `destroy`.
  - `RangeStorage`: `Capacity`, `try_grow`, and `try_shrink`.

But to reiterate, this seems like shoehorning to me considering that:

- A given container has very specific requirements on the Single/Multi and Element/Range axes, and only requires one combination.
- A given storage is tailored to a very specific case on the Single/Multi axis.

So I could see an advantage in carving out a `Storage` with `Handle<T>` and `resolve`. But any further attempt at simplification seems rather artificial for now.

* * *

@RustyYato I don't see how to provide `Drop`:

1. The storage doesn't keep track of which element is initialized, or not, so doesn't know what to `Drop`.
2. The handles would need a mutable reference to the storage to be able to drop, which we can't have if we have multiple handles.
3. In the case of ranges, only the user knows which elements in the range are initialized or not.

So, I don't see any way to call the destructor of elements because of (3), hence the user would be responsible for that regardless. And I don't see any way to release the memory without extra tracking.

I would say that the `Drop` wrapper you ask for is going to be called `Box`, `Vec`, ... I am not sure there's a good opportunity for an intermediate layer.

---

<div class="post-metadata">

### Author: ![RustyYato](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/rustyyato/32/13627_2.png) [@RustyYato](https://internals.rust-lang.org/u/RustyYato)
#### Post date: [February 23, 2021, 2:30pm UTC](https://internals.rust-lang.org/t/is-custom-allocators-the-right-abstraction/13460/84 "2021-02-23T14:30:02Z")

</div>

> [@matthieum](#):
>
> The storage doesn't keep track of which element is initialized, or not, so doesn't know what to `Drop`

It doesn't need to `Drop` the elements, just deallocate the storage if necessary. See `RawVec` in `std` for an example.

---

<div class="post-metadata">

### Author: ![matthieum](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/matthieum/32/4023_2.png) [@matthieum](https://internals.rust-lang.org/u/matthieum)
#### Post date: [February 23, 2021, 4:27pm UTC](https://internals.rust-lang.org/t/is-custom-allocators-the-right-abstraction/13460/85 "2021-02-23T16:27:59Z")

</div>

I am glad we agree that calling `Drop` on the elements is not possible.

How do you plan on solving the MultiStorage issue that it does not track the multiple allocations?

---

<div class="post-metadata">

### Author: ![RustyYato](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/rustyyato/32/13627_2.png) [@RustyYato](https://internals.rust-lang.org/u/RustyYato)
#### Post date: [February 23, 2021, 4:52pm UTC](https://internals.rust-lang.org/t/is-custom-allocators-the-right-abstraction/13460/86 "2021-02-23T16:52:12Z")

</div>

> [@RustyYato](#):
>
> The `Storage` already hands out the `Handle` and provides a release mechanism, why should it matter (for the `Single*` variants) it the release is done in drop or elsewhere? (There doesn't need to be a handle at all for `Single*` `Storage` )

I only mentioned using `Drop` for `Single*`, not `Multi*`. But given @CAD97's comments I think it would be fine to not use `Drop` in this case either.

---

<div class="post-metadata">

### Author: ![matthieum](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/matthieum/32/4023_2.png) [@matthieum](https://internals.rust-lang.org/u/matthieum)
#### Post date: [March 28, 2021, 11:56am UTC](https://internals.rust-lang.org/t/is-custom-allocators-the-right-abstraction/13460/87 "2021-03-28T11:56:11Z")

</div>

As mentioned, there's currently one unsolved issue in storage-poc: RawBox is not CoerceUnsized.

I opened a separate discussion to track this particular issue at [Should Pointee Metadata be CoerceUnsized?](https://internals.rust-lang.org/t/should-pointee-metadata-be-coerceunsized/14352) and would appreciate help in figuring the best way to resolve it.

---

<div class="post-metadata">

### Author: ![irrst](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/irrst/32/7944_2.png) [@irrst](https://internals.rust-lang.org/u/irrst)
#### Post date: [April 7, 2021, 4:30pm UTC](https://internals.rust-lang.org/t/is-custom-allocators-the-right-abstraction/13460/88 "2021-04-07T16:30:41Z")

</div>

I was experimenting with my own PoC for the storage API and I found some things I wanted to share, mention and discuss here.

As I'm not a native english speaker, fell free to ask me if you can't understand some parts! 😉

So these are what I have thought about:

- The distinction between Single\* and Multi\* storage isn't really needed. In general, `Storage` stores information (or inline storage) shared across all allocations and `Handle` stores information about one specific allocation. But for Single\* storages, this distinction is unnecessary because there is no shared information (or shared inline storage) at all. We can actually store everything in handle which would allow that there are many alive handles at any time - which would in turn erase the need for Single\* storage.

- There are some problems with the current typed `Storage` API:

- Currently, we can do nothing with an allocated handle; we need to first acquire the underlying pointer (which may change at any time) to do operations with the memory itself.

Maybe these questions were not needed right now because the current storage proposal is only at the PoC stage; but I think the earlier we raise up unresolved questions, the better we can answer those.

@matthieum: What do you think about this?

* * *

As a side note, thanks for this awesome proposal and working on this kind of stuffs!

---

<div class="post-metadata">

### Author: ![matthieum](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/matthieum/32/4023_2.png) [@matthieum](https://internals.rust-lang.org/u/matthieum)
#### Post date: [April 8, 2021, 4:05pm UTC](https://internals.rust-lang.org/t/is-custom-allocators-the-right-abstraction/13460/89 "2021-04-08T16:05:28Z")

</div>

> [@irrst](#):
>
> What do you think about this?

Some interesting points!

> [@irrst](#):
>
> The distinction between Single\* and Multi\* storage isn't really needed

I'd love to erase it, I'm just not sure _how_.

At the moment, the _only_ difference implementation-wise between Single and Multi is that Single doesn't keep track of whether anything is stored, and Multi does.

This implementation-specific difference, however, is somewhat reflected in the API:

- Single =\> it's up to the caller to remember whether something is stored or not.
- Multi =\> the caller can keep (trying to) allocating and deallocating; if there's not enough room they'll get an error.

So the problem is not storing state in the Handle -- that's already the case for the MultiHandle, they generally store either index or pointer -- but deciding whether the allocation should succeed or fail.

Requiring that Single keep track of whether its storage is occupied or not means requiring that at least one bit of state be available for it, and that one bit is rounded up to the alignment of the storage (at least), so it gets rather costly. In effect, it'll often by an 8 bytes overhead.

Unless I'm mistaken in my reasoning, and if I am please point it out to me. I'd be very happy to erase that distinction if there's no runtime cost.

> [@irrst](#):
>
> compatibility with future custom DST proposals

For now, all those proposals have flunked out. It's hard enough to design an API for a known set of usecases, I'd rather not venture in speculation about an uncertain future.

> [@irrst](#):
>
> dynamic allocations: There are some cases where you want to allocate runtime-sized memory for example like language interpreter, game engines, data driven systems, etc. This is not possible when the API is typed.

Does it?

If you want to get a raw slice of memory, it seems to me you just need a _loose enough_ type. If you have a `MultiRangeStorage`, you can ask for a large slice of `[u8]` and be on your way.

Well, it _is_ currently missing the ability to pass a runtime alignment. I'm not sure if that's a common requirement; if necessary though the `RangeStorage::allocate` call could take a complete layout, rather than just a size, to enable such a usecase.

Of course, if you use raw-memory then you're on the hook for destructing whatever you place there yourself. This seems fair enough to me.

> [@irrst](#):
>
> So the underlying problem is that the current typed `Storage` does not allow runtime-sized allocations.

It does, that's what `RangeStorage` is all about. It may even support resizing existing allocations -- at the cost of potentially invalidating all current handles.

_(Note: `RangeStorage` is enough for `Vec`, and you never know the size of a `Vec` in advance.)_

> [@irrst](#):
>
> One problem arises with this approach: We can't use storages in const context.

I would expect this to be a temporary limitation of const contexts; I'm not too worried about it.

> [@irrst](#):
>
> As a side note, thanks for this awesome proposal and working on this kind of stuffs!

Thank you, I'm glad to see that people find the idea useful!

---

<div class="post-metadata">

### Author: ![riking](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/riking/32/2818_2.png) [@riking](https://internals.rust-lang.org/u/riking)
#### Post date: [April 8, 2021, 6:10pm UTC](https://internals.rust-lang.org/t/is-custom-allocators-the-right-abstraction/13460/90 "2021-04-08T18:10:59Z")

</div>

> [@matthieum](#):
>
> Requiring that Single keep track of whether its storage is occupied or not means requiring that at least one bit of state be available for it, and that one bit is rounded up to the alignment of the storage (at least), so it gets rather costly. In effect, it'll often by an 8 bytes overhead.
> 
> Unless I'm mistaken in my reasoning, and if I am please point it out to me. I'd be very happy to erase that distinction if there's no runtime cost.

At the risk of stating the obvious here, the typical way to manage exclusive access at no runtime cost would be with reference lifetimes - a `Handle` that holds a `&mut Storage`. However, we require that `Handle` be `Copy` so that's not going to work.

> [@irrst](#):
>
> We can actually store everything in handle which would allow that there are many alive handles at any time - which would in turn erase the need for Single\* storage.

This doesn't work for the more esoteric single-element Storage types like `PosixShmemStorage<T>`.

---

<div class="post-metadata">

### Author: ![irrst](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/irrst/32/7944_2.png) [@irrst](https://internals.rust-lang.org/u/irrst)
#### Post date: [April 8, 2021, 10:50pm UTC](https://internals.rust-lang.org/t/is-custom-allocators-the-right-abstraction/13460/91 "2021-04-08T22:50:57Z")

</div>

> [@matthieum](#):
>
> I'd love to erase it, I'm just not sure _how_ .

Let's look at inline storages for example:

Currently, inline buffer is stored in the type implementing `Storage`. This way, you cannot have multiple handles without additional tracking because using one buffer for many handles requires some tracking.

But if the type implementing `Storage` stores _nothing_ and the actual inline buffer is stored in the `Handle`, you _can_ have multiple handles _without_ additional tracking because different handles do _not_ share the same buffer. `Storage` doesn't need to track whether it has already allocated - because the allocation will always succeed regardless of that.

This will require some changes to the API like removing the `Clone + Copy` bounds from `Handle` and passing handles by references instead of by values. But this should be an acceptable trade-off.

_(I know that I'm not very good at explaining something so please ask me again if you still can't see the point. Then I'll try to make a more understandable comment with examples of the implementation.)_

> [@matthieum](#):
>
> So the problem is not storing state in the Handle -- that's already the case for the MultiHandle, they generally store either index or pointer -- but deciding whether the allocation should succeed or fail.

As said above, it will always succeed regardless of how many handles are allocated - at least for single inline storages. I suppose that this will be the case for all other single storages but there may be other cases too.

> [@matthieum](#):
>
> > [@irrst](#):
> >
> > So the underlying problem is that the current typed `Storage` does not allow runtime-sized allocations.
> 
> It does, that's what `RangeStorage` is all about.

Oh, I have overseen that `RangeStorage` allows that. 😅 Then I see nothing anymore which would block `Storage` from being typed.

> [@matthieum](#):
>
> I would expect this to be a temporary limitation of const contexts; I'm not too worried about it.

To be honest, I'm not sure whether this is a temporary limitation because of the problems pointers could cause in const contexts. But given that rust lang devs have already done many things which looked impossible for me, maybe I should not worry about it either. Const collections are not that important anyway.

> [@riking](#):
>
> > [@irrst](#):
> >
> > We can actually store everything in handle which would allow that there are many alive handles at any time - which would in turn erase the need for Single\* storage.
> 
> This doesn't work for the more esoteric single-element Storage types like `PosixShmemStorage<T>` .

I suppose you're talking about shared memory storages (if not, please correct me). I don't think that this doesn't work for shared memory allocators:

You can create different shared memory regions for different handles (with random names). Or you can set permissions in a way which will prevent creating them twice. Either way, you don't need the Single\* traits to avoid overheads because you don't need to track additionally whether you already have allocated or not.

But I know almost nothing about shared memory and I might be overseeing something. Tell me if this is the case.

---

<div class="post-metadata">

### Author: ![matthieum](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/matthieum/32/4023_2.png) [@matthieum](https://internals.rust-lang.org/u/matthieum)
#### Post date: [April 9, 2021, 5:14pm UTC](https://internals.rust-lang.org/t/is-custom-allocators-the-right-abstraction/13460/92 "2021-04-09T17:14:41Z")

</div>

> [@irrst](#):
>
> But if the type implementing `Storage` stores _nothing_ and the actual inline buffer is stored in the `Handle` , you _can_ have multiple handles _without_ additional tracking because different handles do _not_ share the same buffer. `Storage` doesn't need to track whether it has already allocated - because the allocation will always succeed regardless of that.

I had completely missed that in your earlier explanation, and you are correct that this solves the issue of tracking in `Storage`.

I'm not sure, however, that it is applicable to as many usecases as `Storage` (or `Allocator`) currently is.

Think of a Linked List: so, the Storage returns a `Handle<Node>`, now what?

- Where do you store it? You can't store it in a `Node` (infinite size).
- How do nodes refer to it?

> [@irrst](#):
>
> This will require some changes to the API like removing the `Clone + Copy` bounds from `Handle` and passing handles by references instead of by values. But this should be an acceptable trade-off.

I am afraid it isn't.

`Handle<T>` is `Copy` because it seeks to replace `NonNull<T>` which is `Copy`. It's a crucial property for `Handle<T>` to be able to be used wherever `NonNull<T>` is used today; otherwise many collections -- starting from `LinkedList` -- cannot be easily ported.

* * *

A piece of advice: try using your interface.

I didn't realize all the constraints of a good `Storage` API from the get go, they appeared as I tried to use the API in a variety of usecases.

This is the main reason why in storage-poc, there's almost as much code in `collections` than in the actual API + Implementations =\> it's to ensure that the API actually suit the usecases.

The Storage API needs to accommodate a range of usecases:

- `Box<T>` =\> single element, possibly !Sized.
- `Vec<T>` =\> single range, resizable, `Sized` elements.
- `LinkedList<T>` =\> multiple elements, aliased handles, `Sized` elements.
- `SkipList<T>` =\> multiple elements, aliased handles, the elements are `Sized`, but the nodes _shouldn't be_.

I dearly hoped I haven't overlooked a collection type which would impose new requirements/constraints on the storage.

_(Note: SkipList is not implemented; hopefully being that it's just linking Boxes, it should be possible, but once again, maybe I've overlooked something critical)_

---

<div class="post-metadata">

### Author: ![irrst](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/irrst/32/7944_2.png) [@irrst](https://internals.rust-lang.org/u/irrst)
#### Post date: [April 9, 2021, 8:01pm UTC](https://internals.rust-lang.org/t/is-custom-allocators-the-right-abstraction/13460/93 "2021-04-09T20:01:34Z")

</div>

> [@matthieum](#):
>
> A piece of advice: try using your interface.

You're right. I assumed pretty naively that Single\* storages are not needed - only with one implementation for Vec which obviously cannot prove its capability for other use cases. I should definitely implement other collections too to find out whether discarding Single\* storages is possible and ideal. I'll share my experience here if I have some new results.

---

<div class="post-metadata">

### Author: ![system](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/system/32/14092_2.png) [@system](https://internals.rust-lang.org/u/system)
#### Post date: [July 8, 2021, 8:02pm UTC](https://internals.rust-lang.org/t/is-custom-allocators-the-right-abstraction/13460/94 "2021-07-08T20:02:31Z")

</div>

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.

[Previous page](https://internals.rust-lang.org/t/is-custom-allocators-the-right-abstraction/13460.md?page=4)
