-
Notifications
You must be signed in to change notification settings - Fork 12.8k
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
Allocator integration #42313
Allocator integration #42313
Conversation
r? @aturon (rust_highfive has picked a reviewer for you, use r? to override) |
971fd63
to
9f69470
Compare
@pnkfelix while this is waiting for review, looks like there are some failures with debuginfo tests which need updating e.g.
|
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 really happy to see this, and it looks like the integration works quite smoothly, which is great. I certainly don't see any fundamental issues.
I'm a teensy bit disappointed (but not surprised) to not see my grow
/shrink
suggestion (comment); should I submit an RFC for that change?
src/liballoc/allocator.rs
Outdated
ptr: Address, | ||
layout: Layout, | ||
new_layout: Layout) -> Result<(), CannotReallocInPlace> { | ||
let (_, _, _) = (ptr, layout, new_layout); |
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.
Can this check usable_size
and allow the reallocation if the new layout fits within the usable size of the old layout? See also my comment here, which seemed fairly uncontroversial.
src/liballoc/allocator.rs
Outdated
let s = new_layout.size(); | ||
// All Layout alignments are powers of two, so a comparison | ||
// suffices here (rather than resorting to a `%` operation). | ||
if min <= s && s <= max && new_layout.align() <= layout.align() { |
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.
Can this instead just call realloc_in_place
and rely on that to do the usable_size
check? See also my comment here, which seemed fairly uncontroversial.
src/liballoc/allocator.rs
Outdated
let size = size as isize; | ||
let p = self.alloc(layout); | ||
if let Ok(p) = p { | ||
for i in 0..size { |
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.
Can this instead use core::ptr::write_bytes
? That should be more efficient.
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.
Ah yes I briefly skimmed through the core::ptr
source but didn't look at the reexports, so I missed this. Will fix.
src/liballoc/allocator.rs
Outdated
/// not a strict requirement. (Specifically: it is *legal* to use | ||
/// this trait to wrap an underlying native allocation library | ||
/// that aborts on memory exhaustion.) | ||
unsafe fn alloc(&mut self, layout: Layout) -> Result<Address, AllocErr>; |
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 didn't see this mentioned in the RFC discussion, but it occurs to me... why should this be unsafe
? The main reason I can see is that allocators can assume that the layouts they are passed are non-zero, but that ought to be specified in the documentation for alloc
.
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.
Also, that seems more like something to consider for alloc_unchecked
. Should alloc
return Unsupported
for zero-sized allocations?
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.
Hmm. I personally don't think it is a burden to make fn alloc
an unsafe method, since most clients will either need to immediately use unsafe code to initialize the allocated state, or will be passing along an uninitialized block of memory which means that they themselves will probably be unsafe fn's as well...
But I agree that its important to spell out all of the criteria for what safe input arguments are, e.g. requiring layouts have non-zero size...
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.
Actually, now that I've gone and read the man page for malloc
and jemalloc
more carefully, I no longer think that passing a size of zero is undefined behavior...? It says its implementation defined whether you get back a null or a valid non-null address (that you're nonetheless not allowed to dereference), but I don't think it allows for arbitrary UB, does it?
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.
(still, even if its not UB, it doesn't actually solve our own problem, since the alloc::heap
implementation of fn check_size_and_alignment
includes a debug assert that size != 0
, so we would need to either side step that or include an appropriate check up here...)
src/liballoc/allocator.rs
Outdated
/// Creates a layout describing the record for `n` instances of | ||
/// `self`, with a suitable amount of padding between each. | ||
/// | ||
/// Requires non-zero `n` and no arithmetic overflow from inputs. |
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 requirement for non-zero n
is not checked anywhere, even in the (checked) repeat
function. It doesn't really seem necessary (and is counterintuitive for me) that 0 is not allowed.
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.
We have to make a choice about where we are going to attempt to enforce that people do not make zero-sized memory requests to the allocator.
Given how many iterations the API for Layout and Allocator went through, at different times I had different models in my head about whose responsibility it was to ensure that the underlying system allocator did not receive requests for zero-sized memory...
anyway, I'll try to resolve this. not 100% sure which way I'll go (I don't think I have a clear "right direction" from prior discussions on the RFC thread...)
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.
(At this point I'm leaning towards "Layout
can have zero size; it is responsibility of client to ensure either that Layout
it passes to an allocator has positive size, or that it is using an allocator that can handle zero-sized requests.)
src/liballoc/allocator.rs
Outdated
let old_size = layout.size(); | ||
let result = self.alloc(new_layout); | ||
if let Ok(new_ptr) = result { | ||
ptr::copy(ptr as *const u8, new_ptr, cmp::min(old_size, new_size)); |
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 think this can be copy_nonoverlapping
. It would be an invalid allocator implementation to have two outstanding allocations that overlap.
src/liballoc/allocator.rs
Outdated
/// are set to zero before being returned. | ||
unsafe fn alloc_zeroed(&mut self, layout: Layout) -> Result<Address, AllocErr> { | ||
let size = layout.size(); | ||
if size > ::core::isize::MAX as usize { |
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.
Why is this check here?
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.
because one can currently construct a Layout
that overflows isize
?
I suppose I could instead just say that the state of the contents are unspecified if the layout size overflows isize
. Does that seem preferable?
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.
ah it doesn't matter with the switch to ptr::write_bytes
, yay!
src/liballoc/allocator.rs
Outdated
let new_align = cmp::max(self.align, next.align); | ||
let realigned = Layout { align: new_align, ..*self }; | ||
let pad = realigned.padding_needed_for(new_align); | ||
let offset = self.size() + pad; |
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 should probably use checked_add
(and below).
src/liballoc/allocator.rs
Outdated
pub fn padding_needed_for(&self, align: Alignment) -> usize { | ||
debug_assert!(align <= self.align()); | ||
let len = self.size(); | ||
let len_rounded_up = (len + align - 1) & !(align - 1); |
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.
Arguably, this should actually be wrapping arithmetic? That will produce the right answer in all cases, even when len + align
overflows.
src/liballoc/allocator.rs
Outdated
ptr: Address, | ||
layout: Layout, | ||
new_layout: Layout) -> Result<Excess, AllocErr> { | ||
let usable_size = self.usable_size(&new_layout); |
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.
One other possible default implementation would be to copy the realloc
default implementation, but use alloc_excess
instead of alloc
. This would work better if the default realloc
algorithm were used (and alloc_excess
was more precise than usable_size
). However, I don't think it's worth it, since people are probably more likely to just override realloc
and forget about realloc_excess
.
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 think this would only win if you were hooking in an allocator that both
- overrides
fn alloc_excess
, and - does not override
fn realloc_excess
.
Probably better to just advise allocator implementators to understand what the default implementations do when deciding which methods to override.
src/liballoc/heap.rs
Outdated
if usable >= new_layout.size() { Ok(()) } else { Err(CannotReallocInPlace) } | ||
} | ||
|
||
unsafe fn alloc_unchecked(&mut self, layout: Layout) -> Option<*mut u8> { |
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.
Just curious, why not Option<NonZero<*mut u8>>
?
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 early RFC drafts used NonZero
, but there was concern about exposing that as part of the API for this type. See e.g. aturon's comment here
@gereeter wrote:
No, I just forgot to incorporate it into this draft. But it is on the checklist on the description for #32838 and I'll try to put it in while I address the other great feedback you have posted here. |
f21b714
to
d5baa4e
Compare
cc @rust-lang/libs @sfackler @alexcrichton @gereeter any of you want to be official reviewer here? |
Sure, I don't mind reviewing! I read over this and the high-level comments I had were:
Some API nits I also had are below. I'm ambivalent about whether we must resolve these questions before landing.
|
To quote the RFC text:
Returning More minorly, I'm a little worried about efficiency - I see the in-place methods as quick checks that are done first, mostly, and so those don't want a complex error reporting system.
I may be misunderstanding what you mean here, but the amount of padding you need to hit a given alignment will increase with the alignment of the parameter - e.g., an
So, this is interesting because reading your comment I realized that my intuition was wrong. As far as I understand it, the minimum returned from The reasonable case I can think of is in something like See also the motivation comment for including the minimum. |
src/liballoc/allocator.rs
Outdated
if let Ok(()) = self.grow_in_place(ptr, layout.clone(), new_layout.clone()) { | ||
return Ok(ptr); | ||
} | ||
} else if new_size < old_size { |
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.
It seems unfortunate that if new_size == old_size
then this always allocates (when allocating is never necessary). Presumably either one of these two cases should cover that (my vote) or there should be a final else
branch.
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, good point. I know I was also non-plussed by the handling of same size inputs, but cannot remember why I didn't just resolve it in the manner you suggest. (Apart from just being unused to the new grow
/shrink
methods and possibly forgetting if they required the new size to be strictly greater/less than the old one...)
src/liballoc/allocator.rs
Outdated
} | ||
} | ||
|
||
/// The `CannotReallocInPlace` error is used when `fn realloc_in_place` |
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.
Regardless of style, realloc_in_place
has now been replaced by grow_in_place
and shrink_in_place
.
src/liballoc/heap.rs
Outdated
} | ||
} | ||
|
||
unsafe fn grow_in_place(&mut self, |
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 think the heap allocator should have an identical implementation of shrink_in_place
?
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.
Hmm I had thought that there was some reason that the spec for reallocate_inplace
made it hard to detect error. But now I don't know exactly why I had reached that conclusion. Will review.
src/liballoc/heap.rs
Outdated
pub struct HeapAllocator; | ||
|
||
unsafe impl Allocator for HeapAllocator { | ||
unsafe fn alloc(&mut self, layout: Layout) -> Result<*mut u8, AllocErr> { |
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.
Also, is there a reason that this says *mut u8
instead of Address
?
src/liballoc/allocator.rs
Outdated
new_layout: Layout) -> Result<(), CannotReallocInPlace> { | ||
let _ = ptr; // this default implementation doesn't care about the actual address. | ||
debug_assert!(new_layout.size <= layout.size); | ||
if new_layout.align != layout.align { |
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.
Can we just require that new_layout.align == layout.align
for defined behavior? I can't see that this would be hard for users to follow, and it makes sense to me (obviously you won't change the alignment if you don't change the pointer).
This would require adding the check in the default implementation of realloc
, but that doesn't seem to be a problem.
We could just only require that new_layout.align <= layout.align
, but I don't see that being useful for users, and it might be annoying for allocator implementations, since different alignments might result in different size classes.
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 don't have a problem with requiring the old and new alignments to be the same.
// makes it hard to detect failure if it does not hold. | ||
debug_assert!(new_layout.size() >= layout.size()); | ||
|
||
if layout.align() != new_layout.align() { // reallocate_inplace requires 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.
At the very least, the comment here seems incorrect. reallocate_inplace
doesn't even see new_layout.align()
.
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.
... but that's exactly the issue. reallocate_inplace
doesn't take enough inputs to deal with distinct old and new alignments. It just says that its required to take the old alignment as its align
parameter, and that it will produce a block that is aligned according to that.
So if you give this method distinct alignments for old and new, then reallocate_inplace
is not going to be able to satisfy the request.
@alexcrichton I'll add todo's corresponding to your high-level comments to the PR description.
|
(continuing previous comment responding to @alexcrichton 's points...)
|
Hmm. While I personally think an inclusive bound is easy for clients, an exclusive bound would probably be fine for clients too, and would probably be less annoying for implementors... The point about returning 0 is perhaps the only one in favor of keeping the bound inclusive, but so much of the API already says that you're not supposed to pass in layouts of zero-size... In any case, If we're going to revise this we should decide soon. Its the kind of corner-case that will be impossible to revise after the fact... |
Reviewing the API and documentation, I am reminded that the current This was an intuitive definition, and in practice it may be fine, but it may or may not be "the right thing" when it comes to the requirements of arbitrary allocators. In particular, it does not match the requirements of
I.e. this does not allow the @ruuda has been pointing out for quite a while (here and elsewhere) that this is a potential problem with both the Anyway, I point this out mostly to say that the current |
Thanks for the link! Gives me some extra fodder for global allocator traits.
That's a good point! My worry here, though, was that you'd do a lot of work to get a "no" on either the grow/shrink paths and then do more of the same work when you do the full reallocation. You later mentioned that it's pretty reasonable to not implement I'd be ok with this default implementation if we'd assume that allocators which support grow/shrink in place also reimplement
Ok, you're reasoning makes sense to me! If it's normal to fail I wonder if we could consider just returning a In any case this'll likely be an unstable API for awhile and it's otherwise just "one more error type" so it's not that bad either way, I was just curious to get some more rationale written out.
Ok all this about returning a minimum makes sense to me, thanks! The last API question I had was about The actual thing the function does makes total sense of me, I was basically just trying to rationalize why |
src/liballoc/allocator.rs
Outdated
/// padding required to get a 4-aligned address (assuming that the | ||
/// corresponding memory block starts at a 4-aligned address). | ||
/// | ||
/// Behavior undefined if `align` is not a power-of-two. |
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 may be a bit strong for this method, specifically "behavior undefined" in the sense that this method isn't unsafe
so it's technically not allowed to do undefined things for any input. Perhaps rewording this to something like:
The return value of this function has no meaning if
align
is not a power-of-two.
or something like that?
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.
Any reason not to just assert!(align > 0)
?
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 return value of this function has no meaning if
align
is not a power-of-two.
I agree this is a more appropriate way to phrase 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.
From the internal comment:
align is guaranteed to be > 0, so align - 1 is always valid.
So maybe instead have the sentence be:
The return value of this function has no meaning if
align
is not a positive power of two
(also, stylistically, I think it should be "power of two" rather than "power-of-two" (the latter would be appropriate if it were used as an adjective, e.g. "this type has a power-of-two alignment"))
Any reason not to just
assert!(align > 0)
?
Please ignore me; I really don't know what I was thinking when I wrote that 😄
Also FWIW I'm sort of trying to head off differences between this and the global allocators RFC which is currently leaning on a custom trait that's relatively different from this one. I'd personally prefer to stick to one |
I've been working on a crate for a while now that implements a Slab Allocator (among others). It is a single-threaded algorithm, and thus all of my methods are
(side note: I'm not ready to publish the crate yet, so it's still in a private repo, but I'd be happy to add anybody who was curious to take a look) |
📌 Commit 55a629d has been approved by |
⌛ Testing commit 55a629d with merge 2615e54d274d11da3522640bbcce19b0479122f1... |
💔 Test failed - status-travis |
@bors retry timeout on os x builders |
Allocator integration Lets start getting some feedback on `trait Alloc`. Here is: * the `trait Alloc` itself, * the `struct Layout` and `enum AllocErr` that its API relies on * a `struct HeapAlloc` that exposes the system allocator as an instance of `Alloc` * an integration of `Alloc` with `RawVec` * ~~an integration of `Alloc` with `Vec`~~ TODO * [x] split `fn realloc_in_place` into `grow` and `shrink` variants * [x] add `# Unsafety` and `# Errors` sections to documentation for all relevant methods * [x] remove `Vec` integration with `Allocator` * [x] add `allocate_zeroed` impl to `HeapAllocator` * [x] remove typedefs e.g. `type Size = usize;` * [x] impl `trait Error` for all error types in PR * [x] make `Layout::from_size_align` public * [x] clarify docs of `fn padding_needed_for`. * [x] revise `Layout` constructors to ensure that [size+align combination is valid](#42313 (comment)) * [x] resolve mismatch re requirements of align on dealloc. See [comment](#42313 (comment)).
☀️ Test successful - status-appveyor, status-travis |
The Alloc trait for allocators was added in Rust nightly 2017/06/21 (from rust-lang/rust#42313). With the addition of allowing a global allocator to be specified (pending in rust-lang/rust#42727) this will obviate the need for the alloc_uefi crate.
The Alloc trait for allocators was added in Rust nightly 2017/06/21 (from rust-lang/rust#42313). With the addition of allowing a global allocator to be specified (pending in rust-lang/rust#42727) this will obviate the need for the alloc_uefi crate.
The Alloc trait for allocators was added in Rust nightly 2017/06/21 (from rust-lang/rust#42313). With the addition of allowing a global allocator to be specified (pending in rust-lang/rust#42727) this will obviate the need for the alloc_uefi crate.
The Alloc trait for allocators was added in Rust nightly 2017/06/21 (from rust-lang/rust#42313). With the addition of allowing a global allocator to be specified (pending in rust-lang/rust#42727) this will obviate the need for the alloc_uefi crate.
The Alloc trait for allocators was added in Rust nightly 2017/06/21 (from rust-lang/rust#42313). With the addition of allowing a global allocator to be specified (pending in rust-lang/rust#42727) this will obviate the need for the alloc_uefi crate.
Is there a reason that |
@joshlf Well, a reason More succinctly: I didn't think it was a good idea to build in that sort of constraint, especially for a type that I suspect is going to be somewhat niche. |
@pnkfelix Fair enough; that makes sense. |
@pnkfelix Actually, a follow-up on this. What I've been doing in my code is just cloning |
Optimize allocation paths in RawVec Since the `Alloc` trait was introduced (#42313) and it was integrated everywhere (#42727) there's been some slowdowns and regressions that have slipped through. The intention of this PR is to try to tackle at least some of them, but they've been very difficult to quantify up to this point so it probably doesn't solve everything. This PR primarily targets the `RawVec` type, specifically the `double` function. The codegen for this function is now much closer to what it was before #42313 landed as many runtime checks have been elided.
Lets start getting some feedback on
trait Alloc
.Here is:
trait Alloc
itself,struct Layout
andenum AllocErr
that its API relies onstruct HeapAlloc
that exposes the system allocator as an instance ofAlloc
Alloc
withRawVec
an integration ofAlloc
withVec
TODO
fn realloc_in_place
intogrow
andshrink
variants# Unsafety
and# Errors
sections to documentation for all relevant methodsVec
integration withAllocator
allocate_zeroed
impl toHeapAllocator
type Size = usize;
trait Error
for all error types in PRLayout::from_size_align
publicfn padding_needed_for
.Layout
constructors to ensure that size+align combination is valid