-
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
Changes from 1 commit
df69ec1
56d6820
770abea
d80313c
c405fd9
b60f63b
22fe7cb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -79,6 +79,8 @@ are tagged with attributes: | |
/// Behavior is undefined if the requested size is 0 or the alignment is not a | ||
/// power of 2. The alignment must be no larger than the largest supported page | ||
/// size on the platform. | ||
/// | ||
/// This function is required. | ||
#[allocator(allocate)] | ||
pub fn allocate(size: usize, align: usize) -> *mut u8 { | ||
... | ||
|
@@ -92,6 +94,9 @@ pub fn allocate(size: usize, align: usize) -> *mut u8 { | |
/// Behavior is undefined if the requested size is 0 or the alignment is not a | ||
/// power of 2. The alignment must be no larger than the largest supported page | ||
/// size on the platform. | ||
/// | ||
/// This function is optional. If not provided, `allocate` will be called and | ||
/// the resulting buffer will be zerored. | ||
#[allocator(allocate_zeroed)] | ||
pub fn allocate_zeroed(size: usize, align: usize) -> *mut u8 { | ||
... | ||
|
@@ -103,6 +108,8 @@ pub fn allocate_zeroed(size: usize, align: usize) -> *mut u8 { | |
/// | ||
/// The `old_size` and `align` parameters are the parameters that were used to | ||
/// create the allocation referenced by `ptr`. | ||
/// | ||
/// This function is required. | ||
#[allocator(deallocate)] | ||
pub fn deallocate(ptr: *mut u8, old_size: usize, align: usize) { | ||
... | ||
|
@@ -122,6 +129,11 @@ pub fn deallocate(ptr: *mut u8, old_size: usize, align: usize) { | |
/// | ||
/// The `old_size` and `align` parameters are the parameters that were used to | ||
/// create the allocation referenced by `ptr`. | ||
/// | ||
/// This function is optional. If not provided, an implementation will be | ||
/// generated which calls `allocate` to obtain a new buffer, copies the old | ||
/// memory contents to the new buffer, and then calls `deallocate` on the old | ||
/// buffer. | ||
#[allocator(reallocate)] | ||
pub fn reallocate(ptr: *mut u8, old_size: usize, size: usize, align: usize) -> *mut u8 { | ||
... | ||
|
@@ -155,9 +167,9 @@ The allocator functions must be publicly accessible, but can have any name and | |
be defined in any module. However, it is recommended to use the names above in | ||
the crate root to minimize confusion. | ||
|
||
An allocator must provide all functions with the exception of | ||
`reallocate_inplace`. New functions can be added to the API in the future in a | ||
similar way to `reallocate_inplace`. | ||
Note that new functions can be added to this API in the future as long as they | ||
can have default implementations in a similar manner to other optional | ||
functions. | ||
|
||
## Using an allocator | ||
|
||
|
@@ -240,9 +252,47 @@ The allocator APIs could be simplified to a more "traditional" | |
malloc/calloc/free API at the cost of an efficiency loss when using allocators | ||
with more powerful APIs. | ||
|
||
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 commentThe reason will be displayed to describe this comment to others. Learn more. This is not true. Unlike With an eye towards the potential confusion described in the following sentence ("a new instance will be created for each use"), a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Especially if you have access to unstable features, There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe 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 struct MyAllocator;
impl MyAllocator {
fn alloc(&mut self) { }
}
static ALLOCATOR: MyAllocator = MyAllocator;
fn main() {
ALLOCATOR.alloc();
}
@mark-i-m There is no constructor. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Ah, I see. So then, does the
Most allocators need some setup, though. Is the intent to just do something like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, yeah, I totally overlooked the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The RFC has never switched away from the attributes approach. This is an alternative.
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. |
||
since the allocator itself will therefor not be allowed to maintain any state | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Typo: s/therefor/therefore/ |
||
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 commentThe 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 ( There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
period. | ||
|
||
# Unresolved questions | ||
[unresolved]: #unresolved-questions | ||
|
||
It is currently forbidden to pass a null pointer to `deallocate`, though this is | ||
guaranteed to be a noop with libc's `free` at least. Some kinds of patterns in C | ||
are cleaner when null pointers can be `free`d - is the same true for Rust? | ||
|
||
The `Allocator` trait defines several methods that do not have corresponding | ||
implementations here: | ||
|
||
* `oom`, which is called after a failed allocation to provide any allocator | ||
specific messaging that may exist. | ||
* `usable_size`, which is mentioned above as being unused, and should probably | ||
be removed from this trait as well. | ||
* `alloc_excess`, which is like `alloc` but returns the entire usable size | ||
including any extra space beyond the requested size. | ||
* Some other higher level convenience methods like `alloc_array`. | ||
|
||
Should any of these be added to the global allocator as well? It may make sense | ||
to add `alloc_excess` to the allocator API. This can either have a default | ||
implementation which simply calls `allocate` and returns the input size, or | ||
calls `allocate` followed by `reallocate_inplace`. | ||
|
||
The existing `usable_size` function (proposed for removal) only takes a size and | ||
align. A similar, but potentially more useful API is one that takes a pointer | ||
to a heap allocated region and returns the usable size of it. This is supported | ||
as a GNU extension `malloc_useable_size` in the system allocator, and in | ||
jemalloc as well. An [issue][usable_size] has been filed to add this to support | ||
this to aid heap usage diagnostic crates. It would presumably have to return an | ||
`Option` to for allocators that do not have such a function, but this limits | ||
its usefulness if support can't be guaranteed. | ||
|
||
[usable_size]: https://github.com/rust-lang/rust/issues/32075 |
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 offree
ignores it for exactly the reasons mentioned above (more later). That said, passing alignment into thealloc
function is useful (and required for correctness), so I assume that this discussion is mostly about iffree
should takealign
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
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
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
andptr
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. Aptr
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).