Skip to content

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

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
240 changes: 240 additions & 0 deletions src/liballoc/faux.rs
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");
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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.

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 calling abort. If the panic OOMs, well, that sucks but it's not like we really lost anything for trying?

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.

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.

Copy link
Contributor

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?

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.

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 calling abort. If the panic OOMs, well, that sucks but it's not like we really lost anything for trying?

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.

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?

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.

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.

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 reusing Vec<T>.

Copy link
Contributor Author

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's not clear to me whether asking for 2^100 bytes of memory is a logic or runtime error.

I don't see why the API should be complicated by splitting out this error condition from out-of-memory but not all others.

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.

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.

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?

It's going to be a step backwards for performance and is not going to be as clean as simply using Vec 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 reusing Vec.

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.

Copy link
Contributor

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.

You're aborting on a runtime error (out-of-memory) so clearly you don't care about robustness anyway.

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.

It doesn't justify adding a new API with this poor behaviour.

would add extra useless fields to the RingBuf (Vec::len wouldn't be used)

No it would not. I've already implemented it and it doesn't do this.

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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

debug_assert this?

///
/// # 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

debug_assert this

///
/// # 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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will add a lot of overhead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Growth only requires a check for new_size >= requested_size. The usable_size check is only necessary to see if shrinking would have succeeded if heap::reallocate had been used instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Is it reasonable to forbid new_len == old_len?
  • Is it then possible to just check new_size != old_size unconditionally?

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"?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it reasonable to forbid new_len == old_len?

That's not going to work around the issue. There's a whole range of sizes corresponding to the same real size.

Is it then possible to just check new_size != old_size unconditionally?

No, because the returned size is the real size.

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"?

You can check for a shrink failure with the usable_size check. I don't think it's a common use case.

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<(), ()> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be missing something, but how is this different from realloc_array_inplace?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can realloc_array_inplace just delegate to either shrink_in_place or grow_in_place after a cmp (unless strcat complains about the overhead), or be deleted?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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?

Copy link
Contributor

Choose a reason for hiding this comment

The 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>());
}
}
1 change: 1 addition & 0 deletions src/liballoc/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ pub use boxed as owned;
// Heaps provided for low-level allocation strategies

pub mod heap;
pub mod faux;

// Primitive types using the heaps above

Expand Down
34 changes: 5 additions & 29 deletions src/libcollections/ring_buf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ use core::num::{Int, UnsignedInt};
use std::hash::{Writer, Hash};
use std::cmp;

use alloc::heap;
use alloc::faux;

static INITIAL_CAPACITY: uint = 8u; // 2^3
static MINIMUM_CAPACITY: uint = 2u;
Expand Down Expand Up @@ -62,11 +62,7 @@ impl<T> Drop for RingBuf<T> {
fn drop(&mut self) {
self.clear();
unsafe {
if mem::size_of::<T>() != 0 {
heap::deallocate(self.ptr as *mut u8,
self.cap * mem::size_of::<T>(),
mem::min_align_of::<T>())
}
faux::dealloc_array(self.ptr, self.cap);
}
}
}
Expand Down Expand Up @@ -116,18 +112,7 @@ impl<T> RingBuf<T> {
pub fn with_capacity(n: uint) -> RingBuf<T> {
// +1 since the ringbuffer always leaves one space empty
let cap = cmp::max(n + 1, MINIMUM_CAPACITY).next_power_of_two();
let size = cap.checked_mul(mem::size_of::<T>())
.expect("capacity overflow");

let ptr = if mem::size_of::<T>() != 0 {
unsafe {
let ptr = heap::allocate(size, mem::min_align_of::<T>()) as *mut T;;
if ptr.is_null() { ::alloc::oom() }
ptr
}
} else {
heap::EMPTY as *mut T
};
let ptr = unsafe { faux::alloc_array(cap) };

RingBuf {
tail: 0,
Expand Down Expand Up @@ -283,17 +268,8 @@ impl<T> RingBuf<T> {
let count = (new_len + 1).next_power_of_two();
assert!(count >= new_len + 1);

if mem::size_of::<T>() != 0 {
let old = self.cap * mem::size_of::<T>();
let new = count.checked_mul(mem::size_of::<T>())
.expect("capacity overflow");
unsafe {
self.ptr = heap::reallocate(self.ptr as *mut u8,
old,
new,
mem::min_align_of::<T>()) as *mut T;
if self.ptr.is_null() { ::alloc::oom() }
}
unsafe {
self.ptr = faux::realloc_array(self.ptr, self.cap, count);
}

// Move the shortest contiguous section of the ring buffer
Expand Down