-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Prepare global allocators for stabilization #1974
Conversation
Thanks @sfackler! This is an exciting feature to someone who enjoys writing embedded and OS code :) Another pain point not addressed in this RFC is that if you have project that defines its own allocator, you still need define something like the
This seems a bit inflexible, which makes me nervous. It would be nice if crates could indicate whether they absolutely must have allocator X or just would prefer allocator X. Perhaps an additional annotation like
It would be nice if |
Why is |
text/0000-global-allocators.md
Outdated
/// The new size of the allocation is returned. This must be at least | ||
/// `old_size`. The allocation must always remain valid. | ||
/// | ||
/// Behavior is undefined if the requested size is 0 or the alignment is not a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we go for "Behavior is undefined if the requested size is less than old_size
or..."?
It might be worth spelling out explicitly whether old_size
and size
are the only legitimate return values or if the function can also return something inside that range.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I just copied these docs out of alloc::heap
- they need to be cleaned up.
Why not require global allocators to implement the same Allocator trait as is used for collections? I gather that you can't just say |
I'm not sure I understand what the allocator_stub crate is doing. Is the issue you're thinking of a single-crate project that also wants to define its own custom allocator?
If a crate absolutely must have allocator X it can stick
I'm not sure I understand a context in which a crate would want to do this. Could you give an example?
That seems like an implementation bug to me. liballoc shouldn't be doing anything other than telling the compiler that it requires a global allocator.
We could in theory use the |
@comex I'd rather not delay stabilizing this feature until the allocator traits are stabilized. |
text/0000-global-allocators.md
Outdated
/// | ||
/// The `ptr` parameter must not be null. | ||
/// | ||
/// The `old_size` and `align` parameters are the parameters that were used to |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ruuda made a good point in the discussion of the allocator traits: It can be sensible to allocate over-aligned data, but this information is not necessarily carried along until deallocation, so there's a good reason deallocate
shouldn't require the same alignment that was used to allocate.
This requirement was supposed to allow optimizations in the allocator, but AFAIK nobody could name a single existing allocator design that can use alignment information for deallocation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wrote an allocator for an OS kernel once that would have benefited greatly from alignment info.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would be very relevant to both this RFC and the allocators design, so could you write up some details?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm... It seems that I was very mistaken... I have to appologize 🤕
Actually, when I went back and looked at the code, I found the exact opposite. The allocator interface actually does pass the alignment to free
, and my implementation of free
ignores it for exactly the reasons mentioned above (more later). That said, passing alignment into the alloc
function is useful (and required for correctness), so I assume that this discussion is mostly about if free
should take align
or not.
The code is here. It's a bit old and not very well-written since I was learning rust when I wrote it. Here is a simple description of what it does:
Assumptions
- The kernel is the only entity using this allocator. (The user-mode allocator lives in user-mode).
- The kernel is only using this allocator through
Box
, so the parameterssize
andalign
are trusted to be correct, since they are generated by the compiler.
Objective
Use as little metadata as possible.
Blocks
- All blocks are a multiple of the smallest possible block size, which is based on the size of the free-block metadata (16B on a 32-bit machine).
- All blocks have a minimum alignment which is the same as minimum block size (16B).
- The allocator keeps a free-list which is simply a singly linked list of blocks.
- Free blocks are used to store their own metadata.
- Active blocks have no header/footer. This means that their is no header/footer overhead at all.
alloc
Allocating memory just grabs the first free block with required size and alignment, removes it from the free list, splits it if needed, and returns a pointer to its beginning. The size of the block allocated is a function of the alignment and size.
free
Freeing memory requires very little effort, it turns out. Since we assume that the parameters size
and ptr
are valid, we simply create block metadata and add to the linked list. If possible, we can merge with free blocks after the block we are freeing.
In fact, the alignment passed into free is ignored here because the ptr
should already be aligned. The takeaway seems to be the opposite from what I said above (again, sorry). When I thought about it some more, it makes sense. A ptr
inherently conveys some alignment information, so passing this information in as an argument actually seems somewhat redundant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm actually quite relieved to hear that 😄 Yes, allocation and reallocation should have alignment arguments, it's just deallocation that shouldn't use alignment information. It's not quite true that "ptr
inherently conveys alignment information", because the pointer might just happen to have more alignment than was requested, but it's true that it's always aligned as requested at allocation time (since it must be the exact pointer returned by allocation, not a pointer into the allocation).
text/0000-global-allocators.md
Outdated
or more distinct allocator crates are selected, compilation will fail. Note that | ||
multiple crates can select a global allocator as long as that allocator is the | ||
same across all of them. In addition, a crate can depend on an allocator crate | ||
without declaring it to be the global allocator by omitting the `#[allocator]` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it make sense to restrict this choice to "root crates" (executables, staticlibs, cdylibs) analogously to how the panic strategy is chosen? [1] I can't think of a good reason for a library to require a particular allocator, and it seems like it could cause a ton of pain (and fragmentation) to mix multiple allocators within one application.
[1]: It's true that the codegen option -C panic=...
can and must be set for libraries too, but this is mostly to allow separate compilation of crates – the panic runtime to be linked in is determined by the root. There are also restrictions (can't link a panic=abort library into a panic=unwind library). In addition, Cargo exposes only the "root sets panic strategy" usage.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I share this concern. Allowing libraries to require a particular global allocator could create rifts in the crate ecosystem, where different sets of libraries cannot be used together because they require different global allocators.
Allocators share the same interface, and so the optimal allocator will depend on the workload of the binary. It seems like the crate root author will be in the best position to make this choice, since they'll have insight into the workload type, as well as be able to run holistic benchmarks.
Thus is seems like a good idea to restrict global allocator selection to the crate root author.
text/0000-global-allocators.md
Outdated
usage will happen through the *global allocator* interface located in | ||
`std::heap`. This module exposes a set of functions identical to those described | ||
above, but that call into the global allocator. To select the global allocator, | ||
a crate declares it via an `extern crate` annotated with `#[allocator]`: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clarification request: Can all crates do this? As mentioned in another comment, I would conservatively expect this choice to be left to the root crate, as with panic runtimes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As written, any crate can do this, yeah.
I would be fine restricting allocator selection to the root crate if it simplifies the implementation - I can't think of any strong reasons for needing to select an allocator in a non-root crate.
It doesn't have to be a single-crate project, but yes more or less. The idea is that you might have a large crate that both defines and uses an allocator. For example, in an OS kernel, the kernel allocator might want to define this interface so you can use it with
I guess I was thinking that maybe a crate might have performance preference for some allocator without really depending on it. For example, if you know all of your allocations will be of the same size, maybe you would prefer a slab allocator, but it doesn't change correctness if someone else would like a different allocator. TBH, I don't know if anyone actually does this, but it was a thought.
Hmm... That's good to know... I will have to look into this sometime...
Hmm... I don't understand why they should use the same trait. They seem pretty disparate to me... |
In my experience, such specialized allocation behavior is usually implemented by explicitly using the allocator (and having it allocate big chunks of memory from the global allocator). And most allocators used like that aren't suitable as general purpose allocator anyway. |
How are they disparate? The only reason the language needs to have a built-in concept of allocator is for standard library functionality that requires one. Most or all of that functionality should be parameterized by the Here is the pub struct Layout { size: usize, align: usize }
pub unsafe trait Allocator {
// required
unsafe fn alloc(&mut self, layout: Layout) -> Result<*mut u8, AllocErr>;
unsafe fn dealloc(&mut self, ptr: *mut u8, layout: Layout);
// optional but allocator may want to override
fn oom(&mut self, _: AllocErr) -> !;
unsafe fn usable_size(&self, layout: &Layout) -> (usize, usize);
unsafe fn realloc(&mut self, ptr: *mut u8, layout: Layout, new_layout: Layout) -> Result<*mut u8, AllocErr>;
unsafe fn alloc_excess(&mut self, layout: Layout) -> Result<Excess, AllocErr>;
unsafe fn realloc_excess(&mut self, ptr: *mut u8, layout: Layout, new_layout: Layout) -> Result<Excess, AllocErr>
unsafe fn realloc_in_place(&mut self, ptr: *mut u8, layout: Layout, new_layout: Layout) -> Result<(), CannotReallocInPlace>
// plus some convenience methods the allocator probably wouldn't override
}
pub enum AllocErr { Exhausted { request: Layout }, Unsupported { details: &'static str } }
pub struct CannotReallocInPlace; // unit struct Here is your set of functions: pub fn allocate(size: usize, align: usize) -> *mut u8; Exactly equivalent to pub fn allocate_zeroed(size: usize, align: usize) -> *mut u8; Not included in pub fn deallocate(ptr: *mut u8, old_size: usize, align: usize); Exactly equivalent to pub fn reallocate(ptr: *mut u8, old_size: usize, size: usize, align: usize) -> *mut u8; Exactly equivalent to pub fn reallocate_inplace(ptr: *mut u8, old_size: usize, size: usize, align: usize) -> usize; Ditto Overall, there is some functionality missing in yours:
For each of these, The only big difference is that the #[no_mangle]
pub extern fn magic_alloc(layout: Layout) -> Result<*mut u8, AllocErr> {
THE_ALLOC.alloc(layout)
} and the call to
True, but it would arguably be somewhat less weird to have a single attribute than simulating the trait system by enforcing different function signatures (especially if there are extensions in the future, so you have to deal with optional functions). Even if you don't end up literally using the Also, eventually it would be nice to have a proper "forward dependencies" feature rather than special-casing specific types of dependencies (i.e. allocators). This shouldn't wait for that to be stabilized, but it would be nice if in the future the magic attribute could be essentially desugared to a use of that feature, without too much custom logic.
The Allocator RFC has already been accepted (while this one isn't even FCP), and any nitpicks raised regarding the Allocator interface are likely to also apply to global allocators. I don't think you're actually going to save any time. |
@comex Thanks for the clarifications. I had thought you were suggesting not having a global allocator instead of making allocators to implement a trait. I agree that the trait is a more maintainable interface and probably better in the long run... I would stipulate one thing though:
I (the programmer) should be able to define a static mut variable #[global_allocator]
static mut THE_ALLOC: MyAllocator = MyAllocator::new(start_addr, end_addr);
// where MyAllocator: Allocator |
It seems reasonable to make everything but
What are these cases? |
Mmm besides trying to leaverage the trait as much as possible, which I fully support, there was talk in the past of using a general "needs provides" mechanism (some does of applicative functors perhaps) for this, logging, and panicking, and other similar tasks needing a cannonical global singleton. I'd be really disappointed to retreat from that goal into a bunch of narrow mechanisms. |
I don't recall there being any more talk than "hey, would it be possible to make a general needs/provides mechanism". We can't use a thing that doesn't exist. |
@sfackler |
I was referring more specifically to |
In that case, nope, I can't think of a good reason to use |
I based this assertion on the fact that the allocators RFC was accepted with a large number of unresolved questions, and there's been little progress on resolving those. But you're right that most of those questions also apply to the global allocator. |
I've pushed some updates - poke me if I've forgotten anything! cc @nox with respect to what an |
text/0000-global-allocators.md
Outdated
The global allocator could be an instance of the `Allocator` trait. Since that | ||
trait's methods take `&mut self`, things are a bit complicated however. The | ||
allocator would most likely need to be a `const` type implementing `Allocator` | ||
since it wouldn't be sound to interact with a static. This may cause confusion |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not true. Unlike static mut
s, plain static
s are perfectly safe to access and can, in fact, maintain state. It's just that all mutation needs to happen via thread safe interior mutability.
With an eye towards the potential confusion described in the following sentence ("a new instance will be created for each use"), a static
makes much more sense than a const
— the latter is a value that gets copied everywhere, while the former is a unique object with an identity, which seems more appropritate for a global allocator (besides permitting allocator state, as mentioned before).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Especially if you have access to unstable features, static
with interior mutability is idiomatic and can be wrapped in a safe abstraction, while static mut
is quite worse.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree completely. Allocators are inherently stateful since they need to keep track of allocations for correctness. Static + interior mutability is needed.
However, this raises a new question: initializing the global allocator. How does this happen? Is there a special constructor called? Does the constructor have to be a const fn? The RFC doesn't specify this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I may be missing something here, but the issue is that you may not obtain a mutable reference to a static, but every method on Allocator
takes &mut self
:
struct MyAllocator;
impl MyAllocator {
fn alloc(&mut self) { }
}
static ALLOCATOR: MyAllocator = MyAllocator;
fn main() {
ALLOCATOR.alloc();
}
error: cannot borrow immutable static item as mutable
--> <anon>:10:5
|
10 | ALLOCATOR.alloc();
| ^^^^^^^^^
error: aborting due to previous error
@mark-i-m There is no constructor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I may be missing something here, but the issue is that you may not obtain a mutable reference to a static, but every method on Allocator takes &mut self
Ah, I see. So then, does the Allocator
trait have to change? Or do we make the allocator unsafe and use static mut
? If neither is possible, then we might need to switch back to the attributes approach or write a new trait with the hope of coalescing them some time...
@mark-i-m There is no constructor.
Most allocators need some setup, though. Is the intent to just do something like lazy_static
? That would annoy me, but it would work, I guess. Alternately, we could add a method to the interface to do this sort of set up...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, yeah, I totally overlooked the &mut self
issue 😢 We could side-step this by changing the trait (I'm not a fan of that, for reasons I'll outline separately) or by changing how the allocator is accessed. By the latter I mean, for example, tagging a static X: MyAllocator
as the global allocator creates an implicit const X_REF: &MyAllocator = &X;
and all allocation calls get routed through that. This feels extremely hacky, though, and brings back the aforementioned identity confusion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
switch back to the attributes approach
The RFC has never switched away from the attributes approach. This is an alternative.
Most allocators need some setup, though.
Neither system allocators nor jemalloc need explicit setup steps. If you're in an environment where your allocator needs setup, you can presumably call whatever functions are necessary at the start of execution.
text/0000-global-allocators.md
Outdated
internally since a new instance will be created for each use. In addition, the | ||
`Allocator` trait uses a `Layout` type as a higher level encapsulation of the | ||
requested alignment and size of the allocation. The larger API surface area | ||
will most likely cause this feature to have a significantly longer stabilization |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not so sure about this any more. At least the piece of the API surface named here (Layout
) doesn't seem very likely to delay anything. I don't recall any unresolved questions about it (there's questions about what alignment means for some functions, but that's independent of whether it's wrapped in a Layout
type).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The allocators RFC hasn't even been implemented yet. We have literally zero experience using the Allocator
trait or the Layout
type. In contrast, alloc::heap
and the basic structure of global allocators have been implemented and used for the last couple of years.
@sfackler I think that's more for lack of time than interest. An now Haskell's "backpack" basically wrote the book on how to retrofit a module system on a language without one (and with type classes / traits), so it's not like research is needed. I'm fine with improving how things work on an experimental basis, but moving towards stabilization seems vastly premature---we haven't even implemented our existing allocator RFC! |
So, using the allocator trait naturally suggests a @mark-i-m brought up the possibility of changing the trait to take Contrast this with the global allocator, where you can't just hand out a handle to every user, because users are everywhere. One could define an ad-hoc scheme to automatically introduce such handles (e.g., with To me, this mismatch between "local" allocators and global ones is a strong argument to not couple the latter to the trait used for the former. |
@rkruppe the handle thing is correct. My convention the global allocator handles it's own synchronization, but that's the only magic. |
@Ericson2314 I'm not sure I catch your drift. The global allocator (edit: by this I mean the type implementing |
rustc: Implement the #[global_allocator] attribute This PR is an implementation of [RFC 1974] which specifies a new method of defining a global allocator for a program. This obsoletes the old `#![allocator]` attribute and also removes support for it. [RFC 1974]: rust-lang/rfcs#1974 The new `#[global_allocator]` attribute solves many issues encountered with the `#![allocator]` attribute such as composition and restrictions on the crate graph itself. The compiler now has much more control over the ABI of the allocator and how it's implemented, allowing much more freedom in terms of how this feature is implemented. cc #27389
rustc: Implement the #[global_allocator] attribute This PR is an implementation of [RFC 1974] which specifies a new method of defining a global allocator for a program. This obsoletes the old `#![allocator]` attribute and also removes support for it. [RFC 1974]: rust-lang/rfcs#1974 The new `#[global_allocator]` attribute solves many issues encountered with the `#![allocator]` attribute such as composition and restrictions on the crate graph itself. The compiler now has much more control over the ABI of the allocator and how it's implemented, allowing much more freedom in terms of how this feature is implemented. cc #27389
How does one use a https://github.com/rust-lang/rust/blob/master/src/libstd/heap.rs |
@tarcieri it's not intended currently to be able to implement |
@alexcrichton yeah got that working today, was just curious if there was something better I could do. |
Rendered