-
Notifications
You must be signed in to change notification settings - Fork 12.9k
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
fallible allocator experiment #111970
fallible allocator experiment #111970
Conversation
r? @thomcc (rustbot has picked a reviewer for you, use r? to override) |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
uses separate trait so there's no generic associated types
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment was marked as outdated.
This comment was marked as outdated.
@bors try @rust-timer queue |
This comment has been minimized.
This comment has been minimized.
⌛ Trying commit 0357918 with merge 1a267bc685998caa7666938acb14302f1cd729b6... |
☀️ Try build successful - checks-actions |
This comment has been minimized.
This comment has been minimized.
Finished benchmarking commit (1a267bc685998caa7666938acb14302f1cd729b6): comparison URL. Overall result: ❌✅ regressions and improvements - ACTION NEEDEDBenchmarking this pull request likely means that it is perf-sensitive, so we're automatically marking it as not fit for rolling up. While you can manually mark this PR as fit for rollup, we strongly recommend not doing so since this PR may lead to changes in compiler perf. Next Steps: If you can justify the regressions found in this try perf run, please indicate this with @bors rollup=never Instruction countThis is a highly reliable metric that was used to determine the overall result at the top of this comment.
Max RSS (memory usage)ResultsThis is a less reliable metric that may be of interest but was not used to determine the overall result at the top of this comment.
CyclesResultsThis is a less reliable metric that may be of interest but was not used to determine the overall result at the top of this comment.
Binary sizeResultsThis is a less reliable metric that may be of interest but was not used to determine the overall result at the top of this comment.
Bootstrap: 648.715s -> 661.923s (2.04%) |
(drive-by comment, saw this since I'm involved with storages) This is, indeed, very cool, but also a notable increase in incidental complexity that everybody needs to deal with, not just those using fallible-mode allocation. Everyone gets exposed to it, because the docs for fn push_back(&mut self, value: T) will now be instead docs for fn push_back(
&mut self,
value: T,
) -> <<A as Allocator>::ErrorHandling as ErrorHandling>::Result<(), TryReserveError> (See for example [ This sort of double projection is already currently under fire as needlessly complex and unprecedented (in std) with It is certainly very cool that double type projection can enable Rust to model monadic type rebinding, but including it into stable Rust will be an uphill battle at best. Footnotes
|
To be fair, I see no reason why I originally started with |
Agreed that they look messy, but I'm wondering if this might be something that |
I ended up rambling about possibilities in the storages RFC thread; linking that here in the possibility that it's of some use to someone. rust-lang/rfcs#3446 (comment) TL;DR: documenting with a stricter bound for
I agree — qualifying the first projection already isn't necessary, but the compiler always complains that the second projection is ambiguous despite only one version of the name being resolvable — but it's worth noting that rustdoc currently just always uses the qualified form, because it doesn't have any concept of "in scope" traits for which to permit the unqualified projection. So unless rustdoc changes that rule somehow (e.g. considering generic bounds as the "in scope" traits to elide qualification for (which might be what you were saying) or just matching whatever the source uses), it's going to continue documenting using the qualified form even if the source uses an unqualified projection. Projections aren't always guaranteed to be unambiguous even when they're on a generic type, because there could be a blanket-implemented trait in scope which is satisfied by the bound and provides the same associated name. |
"Doc switches" are quite interesting because they could potentially be used for other purposes too. I suggested groups for hiding/collapsing particular API sets by default (e.g. for
Sounds like providing sample template arguments for C++ IntelliSense in Visual Studio, but for Rust docs. There is a UI GIF at https://devblogs.microsoft.com/cppblog/template-intellisense/. Especially useful if one could save/pass a URL with those filters set in a fragment. |
I think one thing we could do is just have separate impl blocks for the different forms of error handling: impl<T, A> Vec<T, A>
where A: Allocator<ErrorHandling = Fatal>
{
fn push(&mut self) { ... }
}
impl<T, A> Vec<T, A>
where A: Allocator<ErrorHandling = Fallible>
{
fn push(&mut self) -> Result<(), TryReserveError> { ... }
} That would keep the signatures more sane. Rustdoc already splits function signatures by impl block. Maybe they could add a (The above code currently results in a duplicate definition error but I don't see why it must) |
Has any thought been given in moving fallibility outside of the How do deal with a failed allocation seems to be more of a matter of usecase, than a matter of allocator. Certainly, one can wrap a fallible allocator into an infallible one (aborting on failure) and vice versa (just Ok wrapping) but... considering that most allocators are fallible in the first place, this seems like a round-about way to go about things, and a conflation of responsibilities. Mixing multiple responsibilities in a single entity generally violates the Single Responsibility Principle, which typically leads to undesirable consequences. What if, instead, a policy-based design was used? That is:
The policy can be as simple as taking the result of the allocation, and either returning just the pointer (infallible) or returning a impl<T, A, AFP> Vec<T, A, AFP>
where
A: Allocator,
AFP: AllocationFailurePolicy<Result = NonNull<u8>>,
{
fn push(&mut self) { ... }
}
impl<T, A, AFP> Vec<T, A, AFP>
where
A: Allocator,
AFP: AllocationFailurePolicy<Result = Result<NonNull<u8>, AllocError>>,
{
fn push(&mut self) -> Result<(), AllocError> { ... }
} Which has the advantage that writers of allocators don't have to worry about, and users of allocators only need to learn about a handful of allocation failure policies no matter how many allocators they use: a typical M + N (vs M x N) benefit of separating orthogonal concepts. Finally #[doc(inline)]
pub Vec<T, A> = alloc::Vec<T, A, Abort>; So that by default the doc for |
Error handling is restricted to be either Additionally, while all allocators can fail, certain allocators are more likely to fail than others. While the A separate param does have the advantage of being easier to file off for the std facade, since it's just fixing a generic instead of refining a trait bound. |
I was tempted into trying that kind of scheme out, though, and the following worked, or at least compiled: #![feature(never_type, try_blocks, yeet_expr)]
pub trait ErrorHandling: Sealed {
#[must_use]
type FailWith<E>;
#[must_use]
type Output<T, E>;
fn cover<T, E>(result: Result<T, Self::FailWith<E>>) -> Self::Output<T, E>;
fn fail<E>(error: E) -> Self::FailWith<E>;
fn unwrap_with<T, E>(
result: Result<T, E>,
op: impl FnOnce(E) -> !,
) -> Result<T, Self::FailWith<E>>;
}
impl ErrorHandling for Fatal {
type FailWith<E> = !;
type Output<T, E> = T;
fn cover<T, E>(result: Result<T, !>) -> Self::Output<T, E> {
match result {
Ok(result) => result,
Err(never) => never,
}
}
fn fail<E>(_: E) -> Self::FailWith<E> {
unreachable!()
}
fn unwrap_with<T, E>(
result: Result<T, E>,
op: impl FnOnce(E) -> !,
) -> Result<T, Self::FailWith<E>> {
result.map_err(op)
}
}
impl ErrorHandling for Nonfatal {
type FailWith<E> = E;
type Output<T, E> = Result<T, E>;
fn cover<T, E>(result: Result<T, E>) -> Self::Output<T, E> {
result
}
fn fail<E>(error: E) -> Self::FailWith<E> {
error
}
fn unwrap_with<T, E>(
result: Result<T, E>,
_: impl FnOnce(E) -> !,
) -> Result<T, Self::FailWith<E>> {
result
}
}
#[cfg(not(no_global_oom_handling))]
type DefaultEh = Fatal;
#[cfg(no_global_oom_handling)]
type DefaultEh = Nonfatal;
pub struct Vec<T, A: Allocator = Global, EH: ErrorHandling = DefaultEh> {
ptr: NonNull<T>,
len: usize,
cap: usize,
alloc: A,
eh: PhantomData<EH>,
}
impl<T, A: Allocator, EH: ErrorHandling> Vec<T, A, EH> {
pub fn with_capacity_in(cap: usize, alloc: A) -> EH::Output<Self, A> {
EH::cover(
try {
let Ok(layout) = EH::unwrap_with(Layout::array::<T>(cap), |_| capacity_overflow())
else {
do yeet EH::fail(alloc);
};
let Ok(ptr) =
EH::unwrap_with(alloc.allocate(layout), |_| handle_alloc_error(layout))
else {
do yeet EH::fail(alloc);
};
Self {
ptr: ptr.cast(),
len: 0,
cap,
alloc,
eh: PhantomData,
}
},
)
}
} It's not perfectly pretty, but it's another potential direction. Also I did try but |
Closing this as it was an experiment and has conflicts |
No description provided.