-
Notifications
You must be signed in to change notification settings - Fork 13.4k
WIP: Add Faux allocator API #19320
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
WIP: Add Faux allocator API #19320
Changes from all commits
d9d86b7
7eaf467
2164d02
a438117
3235ef5
4ec9b2e
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 |
---|---|---|
@@ -0,0 +1,240 @@ | ||
// Copyright 2014 The Rust Project Developers. See the COPYRIGHT | ||
// file at the top-level directory of this distribution and at | ||
// http://rust-lang.org/COPYRIGHT. | ||
// | ||
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or | ||
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license | ||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your | ||
// option. This file may not be copied, modified, or distributed | ||
// except according to those terms. | ||
|
||
use heap::{mod, usable_size, EMPTY}; | ||
use core::mem::{size_of, min_align_of}; | ||
use core::ptr::null_mut; | ||
use core::num::Int; | ||
use core::result::Result; | ||
use core::result::Result::{Ok, Err}; | ||
|
||
/// Allocates and returns a ptr to memory to store a single element of type T. Handles zero-sized | ||
/// types automatically by returning the non-null EMPTY ptr. | ||
/// | ||
/// # Aborts | ||
/// | ||
/// Aborts on OOM | ||
#[inline] | ||
pub unsafe fn alloc<T>() -> *mut T { | ||
let size = size_of::<T>(); | ||
if size == 0 { | ||
EMPTY as *mut T | ||
} else { | ||
let ptr = heap::allocate(size, min_align_of::<T>()) as *mut T; | ||
if ptr.is_null() { ::oom(); } | ||
ptr | ||
} | ||
} | ||
|
||
/// Allocates and returns a ptr to memory to store a `len` elements of type T. Handles zero-sized | ||
/// types automatically by returning the EMPTY ptr. | ||
/// | ||
/// # Undefined Behaviour | ||
/// | ||
/// * `len` must not be 0. | ||
/// | ||
/// # Panics | ||
/// | ||
/// Panics if size_of<T> * len overflows. | ||
/// | ||
/// # Aborts | ||
/// | ||
/// Aborts on OOM | ||
#[inline] | ||
pub unsafe fn alloc_array<T>(len: uint) -> *mut T { | ||
debug_assert!(len != 0, "0 len passed to alloc_array"); | ||
let size = size_of::<T>(); | ||
if size == 0 { | ||
EMPTY as *mut T | ||
} else { | ||
let desired_size = size.checked_mul(len).expect("capacity overflow"); | ||
let ptr = heap::allocate(desired_size, min_align_of::<T>()) as *mut T; | ||
if ptr.is_null() { ::oom(); } | ||
ptr | ||
} | ||
} | ||
|
||
/// Resizes the allocation referenced by `ptr` to fit `len` elements of type T. Handles zero-sized | ||
/// types automatically by returning the given ptr. `old_len` must be then `len` provided to the | ||
/// call to `alloc_array` or `realloc_array` that created `ptr`. | ||
/// | ||
/// # Undefined Behaviour | ||
/// | ||
/// * `len` must not be 0. | ||
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.
|
||
/// | ||
/// # Panics | ||
/// | ||
/// Panics if size_of<T> * len overflows. | ||
/// | ||
/// # Aborts | ||
/// | ||
/// Aborts on OOM | ||
#[inline] | ||
pub unsafe fn realloc_array<T>(ptr: *mut T, old_len: uint, len: uint) -> *mut T { | ||
debug_assert!(len != 0, "0 len passed to realloc_array"); | ||
let size = size_of::<T>(); | ||
if size == 0 { | ||
ptr | ||
} else { | ||
let desired_size = size.checked_mul(len).expect("capacity overflow"); | ||
let align = min_align_of::<T>(); | ||
// No need to check old_size * len, must have been checked when the ptr was made, or | ||
// else UB anyway. | ||
let ptr = heap::reallocate(ptr as *mut u8, size * old_len, desired_size, align) as *mut T; | ||
if ptr.is_null() { ::oom(); } | ||
ptr | ||
} | ||
|
||
} | ||
|
||
/// Tries to resize the allocation referenced by `ptr` in-place to fit `len` elements of type `T`. | ||
/// If successful, yields `Ok`. If unsuccessful, yields `Err`, and the allocation is unchanged. | ||
/// Handles zero-sized types by always returning `Ok`. | ||
/// | ||
/// # Undefined Behaviour | ||
/// | ||
/// * `old_len` must be the `len` provided to the last successful allocator call that created or | ||
/// changed `ptr`. | ||
/// * `len` must not be 0. | ||
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.
|
||
/// | ||
/// # Panics | ||
/// | ||
/// Panics if size_of<T> * len overflows. | ||
#[inline] | ||
pub unsafe fn realloc_array_inplace<T>(ptr: *mut T, old_len: uint, len: uint) -> Result<(), ()> { | ||
debug_assert!(len != 0, "0 len passed to realloc_array_inplace"); | ||
// FIXME: just remove this in favour of only shrink/grow? | ||
let size = size_of::<T>(); | ||
let align = min_align_of::<T>(); | ||
if size == 0 { | ||
Ok(()) | ||
} else { | ||
let desired_size = size.checked_mul(len).expect("capacity overflow"); | ||
// No need to check old_size * len, must have been checked when the ptr was made, or | ||
// else UB anyway. | ||
let result_size = heap::reallocate_inplace(ptr as *mut u8, size * old_len, | ||
desired_size, align); | ||
if result_size == usable_size(desired_size, align) { | ||
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 will add a lot of overhead. 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. Hmm, I was under the impression it was necessary to check. Or is it only for shrinks? If that's the case, it might make sense to split this into grow and shrink? 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. Growth only requires a check for 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 docs seem unclear to me on how to evaluate if a shrink is successful, whatever that really means. Or is that just something we shouldn't care about? Like if a shrink fails "oh well"? 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.
That's not going to work around the issue. There's a whole range of sizes corresponding to the same real size.
No, because the returned size is the real size.
You can check for a shrink failure with the |
||
Ok(()) | ||
} else { | ||
Err(()) | ||
} | ||
} | ||
} | ||
|
||
/// Tries to grow the allocation referenced by `ptr` in-place to fit `len` elements of type `T`. | ||
/// If successful, yields `Ok`. If unsuccessful, yields `Err`, and the allocation is unchanged. | ||
/// Handles zero-sized types by always returning `Ok`. | ||
/// | ||
/// # Undefined Behaviour | ||
/// | ||
/// * `old_len` must be the `len` provided to the last successful allocator call that created or | ||
/// changed `ptr`. | ||
/// * `len` must not be 0. | ||
/// * `len` must not be smaller than `old_len`. | ||
/// | ||
/// # Panics | ||
/// | ||
/// Panics if size_of<T> * len overflows. | ||
#[inline] | ||
pub unsafe fn try_grow_inplace<T>(ptr: *mut T, old_len: uint, len: uint) -> Result<(), ()> { | ||
debug_assert!(len >= old_len, "new len smaller than old_len in try_grow_inplace"); | ||
let size = size_of::<T>(); | ||
let align = min_align_of::<T>(); | ||
if size == 0 { | ||
Ok(()) | ||
} else { | ||
let desired_size = size.checked_mul(len).expect("capacity overflow"); | ||
// No need to check size * old_len, must have been checked when the ptr was made, or | ||
// else UB anyway. | ||
let result_size = heap::reallocate_inplace(ptr as *mut u8, size * old_len, | ||
desired_size, align); | ||
if result_size >= desired_size { | ||
Ok(()) | ||
} else { | ||
Err(()) | ||
} | ||
} | ||
} | ||
|
||
/// Tries to shrink the allocation referenced by `ptr` in-place to fit `len` elements of type `T`. | ||
/// If successful, yields `Ok`. If unsuccessful, yields `Err`, and the allocation is unchanged. | ||
/// Handles zero-sized types by always returning `Ok`. | ||
/// | ||
/// # Undefined Behaviour | ||
/// | ||
/// * `old_len` must be the `len` provided to the last successful allocator call that created or | ||
/// changed `ptr`. | ||
/// * `len` must not be 0. | ||
/// * `len` must not be larger than `old_len`. | ||
/// | ||
/// # Panics | ||
/// | ||
/// Panics if size_of<T> * len overflows. | ||
#[inline] | ||
pub unsafe fn try_shrink_inplace<T>(ptr: *mut T, old_len: uint, len: uint) -> Result<(), ()> { | ||
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 might be missing something, but how is this different from 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 am considering deprecating realloc_array_inplace in favour of shrink/grow. That way they can be optimized accordingly. try_grow_inplace does a simpler size check for "success" and shrink_in_place omits a checked_mul, assuming they indeed passed in a smaller value than old_len. (I'm shaky on what exactly should be checked in both, though). 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. Can 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'd prefer deleted at this point in time, but am willing to be convinced otherwise. There might be a usecase where it's unclear by the caller if they want grow or shrink? 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. shrug I can't think of any, but it can always be added again later! |
||
debug_assert!(len != 0, "0 len passed to try_shrink_inplace"); | ||
debug_assert!(len <= old_len, "new len bigger than old_len in try_grow_inplace"); | ||
let size = size_of::<T>(); | ||
let align = min_align_of::<T>(); | ||
if size == 0 { | ||
Ok(()) | ||
} else { | ||
// No need to check either mul, size * len <= size * old_len, and size * old_len must have | ||
// been checked when the ptr was made, or else UB anyway. | ||
let desired_size = size * len; | ||
let result_size = heap::reallocate_inplace(ptr as *mut u8, size * old_len, | ||
desired_size, align); | ||
if result_size == usable_size(desired_size, align) { | ||
Ok(()) | ||
} else { | ||
Err(()) | ||
} | ||
} | ||
} | ||
|
||
|
||
/// Deallocates the memory referenced by `ptr`, assuming it was allocated with `alloc`. | ||
/// Handles zero-sized types automatically by doing nothing. | ||
/// | ||
/// # Undefined Behaviour | ||
/// | ||
/// * The `ptr` must have been allocated by this API's `alloc` method. | ||
/// * The `ptr` must not have been previously deallocated. | ||
#[inline] | ||
pub unsafe fn dealloc<T>(ptr: *mut T) { | ||
let size = size_of::<T>(); | ||
if size == 0 { | ||
// Do nothing | ||
} else { | ||
heap::deallocate(ptr as *mut u8, size, min_align_of::<T>()); | ||
} | ||
} | ||
|
||
/// Deallocates the memory referenced by `ptr`, assuming it was allocated with `alloc_array` or | ||
/// `realloc_array`. Handles zero-sized types automatically by doing nothing. | ||
/// | ||
/// # Undefined Behaviour | ||
/// | ||
/// * The `ptr` must have been allocated by this API's `alloc_array` or `realloc_array` methods. | ||
/// * The `ptr` must not have been previously deallocated. | ||
/// * `len` must be the `len` provided to the last successful allocator call that created or | ||
/// changed `ptr`. | ||
#[inline] | ||
pub unsafe fn dealloc_array<T>(ptr: *mut T, len: uint) { | ||
let size = size_of::<T>(); | ||
if size == 0 { | ||
// Do nothing | ||
} else { | ||
// No need to check size * len, must have been checked when the ptr was made, or | ||
// else UB anyway. | ||
heap::deallocate(ptr as *mut u8, size * len, min_align_of::<T>()); | ||
} | ||
} |
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 should be considered out-of-memory. It's really no different than the internal allocator calculations.
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's a reasonable choice, yeah. Although it "feels" less catastrophic than the allocator explicitly declaring OOM. As in I expect it more likely to be recoverable (in the sense that the application can cleanly shut down via unwinding) than an explicit OOM.
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.
A size overflow inside of the allocator isn't less recoverable than a size overflow outside of 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.
One is "you asked for more memory than can possibly be allocated by this model of computation", and the other is "the allocator can't make your object (even if you only asked for a single byte)". While in practice the latter is probably just the former (you still have tons of memory, you just asked for something too big), that's not necessarily the case.
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.
An out-of-memory condition is a runtime error so panics aren't an appropriate way to handle it anyway. Any case where runtime errors can only be handled via exceptions would make the dialect without exceptions second-class. Rust's implementation of exceptions dynamically allocates memory so it would be unreliable too. I think use cases where out-of-memory needs to be handled are important and I don't like the idea of a solution solving 5% of the problem and discouraging a proper implementation.
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.
An out-of-memory error will also be returned for cases where the allocator could never succeed due to the mix of size / alignment requirements.
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 sure I follow your reasoning here, can you elaborate? Do we have any written guidelines for what should panic vs abort? Or are you picturing a future where calling
alloc::oom
is configurable to do something more useful than aborting?I agree that there are plenty of cases that are the same error. That's why I noted the "you still have tons of memory, you just asked for something too big" case. However if the allocator spits out null, we have no way (that I'm aware of) to identify the problem. We don't know if we were just stupid and asked for too much or a weird align/size combination, or if the system is genuinely out of space for a very reasonable request.
However in the case of our
expect
, we know for a fact that it was the parent asking for more memory than can even exist, and we can provide more info than just callingabort
. If the panic OOMs, well, that sucks but it's not like we really lost anything for trying?I am also interested in handling these usecases in a reasonable way. My inquiries in to this matter with other team members has amounted to "that's not a problem the standard library should try to handle gracefully, just bail out with panic/abort". Can you elaborate on, or direct me to, what you believe the right solution to be?
Note that the point of this API isn't to provide a longterm solution to All The Allocation Problems. Just to cleanup up our current implementations while we wait for a better allocator API to arrive.
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.
Panic is supposed to be for logic errors and using it for a runtime error like out-of-memory would be misuse. It can't be our strategy for handling out-of-memory because it would prevent first tier support for disabling unwinding and will force losing an entire thread.
I don't see why the API should be complicated by splitting out this error condition from out-of-memory but not all others. This is also a performance critical API and you're adding a significant amount of code bloat for a very dubious reason.
The right solution is bubbling up the out-of-memory errors. Panics are not an acceptable solution to runtime errors. I think it's unacceptable to force people to use unwinding to have a not-even-half-working way of handling OOM. It's worse to provide a 5% solution to this problem that's going to get in the way of future progress than it is to simply abort.
It's going to be a step backwards for performance and is not going to be as clean as simply using
Vec<T>
inside of the ring buffer implementation. I don't see the need for this extra complexity / code bloat to factor out a couple calls to the lower level API. It's not an improvement over reusingVec<T>
.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's not clear to me whether asking for 2^100 bytes of memory is a logic or runtime error.
It's the current semantics all of our collections have though. If you ask for more capacity than a uint can handle, they panic with "capacity overflow". ::oom'ing would change from the current behaviour.
Can you elaborate on the design you imagine? I believe we discussed this on IRC before, and I don't recall any concrete ideas. Bubbling the errors via Results on every-operation-that-can-allocate would almost certainly have a performance impact, so I'm assuming you have a more robust solution in mind?
I'm not convinced of this at all. Using Vec would be fairly awkward from what I picture the implementation being, would add extra useless fields to the RingBuf (Vec::len wouldn't be used), and would likely involve duplicate logic. As for performance compared to the current impl, the only extra work I do now that the current RingBuf doesn't is check for len == 0, which I'm strongly considering removing in favour of just declaring it UB.
Regardless, I'll prototype out a couple different impls/benches to compare perf/design.
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.
You're aborting on a runtime error (out-of-memory) so clearly you don't care about robustness anyway.
It doesn't justify adding a new API with this poor behaviour.
No it would not. I've already implemented it and it doesn't do this.