-
Notifications
You must be signed in to change notification settings - Fork 9
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
Ban zero-sized allocations #16
Comments
If we do this, it might make sense to change the types that That would basically turn |
I don't understand why zero sized allocations are banned if the output is a result anyway. Can't an allocator that disallows zero sized allocation just return an err if given a zero sized layout? |
How about adding a While most allocators have no reason to deal with ZST's, a minority will want to intercept such allocations. Equally, for most code it's must easier if the allocator API can handle ZST's and treat them like any other type. So I think a default implementation that works for 99% of use-cases will save everyone some work. |
(feel free to bikeshed the name; |
My personal preference is to always require allocators to support zero-sized allocations, which keeps things simple. Some allocators such as bumpalo don't need any special work to support this, while others can simply cast the alignment to a pointer and return that if the size is zero. |
Shall we introduce a Currently, zeroed allocation is pretty error prone and even experienced users may miss this: rust-lang/rust#63291 (comment). On the other hand, we would restrict implementations which are fine to get zeroed layout like a bump allocator.
Please vote with
|
Disclaimer: I am just spitballing to get some feedback. In general, Rust's philosophy is to provide safe, zero-cost abstractions; while also allowing unsafe behavior at the cost of some additional effort from the programmer. According to that philosophy, the basic behavior of an allocator should detect and prevent mistakes that are easily overlooked by the programmer. Zero-sized allocations seem like an instance of the aforementioned, so it might be a good idea to create a special syntax for it. |
I think this would be a good new issue if this proposal gets rejected. For methods like Another way to solve this dilemma is to pass I didn't test those approaches so far, these are only random thoughts, so take it with a grain of salt. |
It's nonsensical to allocate a zero sized type. The AllocRef trait should not allow something nonsensical. It should be the lowest level cross-platform API to any allocator, including those that don't account for zero sized allocations. It might make sense to add an extension trait with some sort of logic for handling zero sized allocations, but what should it do? Return a unique pointer for each allocation? Return a unique pointer for each distinct zero-sized layout? It's application specific. |
Allocating a zero sized type is perfectly well defined. Each distinct memory address can contain an infinite number of ZSTs, so the allocator can just return any arbitrary address if the allocation size is zero. The only constraint is that ZSTs may have an alignment requirement (e.g. zero-length array), which is why the simplest way to handle them is to cast the alignment part of the |
That assumes you have nothing at those memory locations that you care about, which is a bad assumption on embedded devices. And, in my opinion, part of the goal of this working group should be to support allocation on embedded devices. |
No, it literally doesn't matter what is at those memory locations because that memory is never accessed. Dereferencing a ZST doesn't actually access any memory. |
Also keep in mind that it's perfectly fine for multiple ZSTs to share the same memory address. |
I guess I just don't understand the semantics of ZSTs in Rust. What's the best place for me to read up on them? |
https://doc.rust-lang.org/nomicon/vec-zsts.html seems to have a pretty good coverage of the topic. There's also https://doc.rust-lang.org/nomicon/exotic-sizes.html#zero-sized-types-zsts as a more general introduction to ZSTs. |
There is not now, nor has there ever been, a reason to make zero-sized allocations UB. |
Okay, thank you for the links. After reading them, I agree that the standard That being said, I don't think we should require implementors to handle that case. So I propose providing a default impl for |
If we did a split like that would the main alloc method then be marked safe? I don't want complexity for no reason, but if it lets us give the "normal" allocation path a safe route that'd be worth it. Otherwise, I can't imagine it's any sort of performance penalty. If the size requested is zero you cast the alignment to a pointer and off you go. I don't think it's worth complicating the API for a single |
@scottjmaddox Do I understand correctly you want to introduce at least two methods: @Lokathor The I think we should close this proposal and make a new one for the splitting things. With that proposal we may explicitly allow to pass zero to |
alloc_nonempty |
Note how a default alloc impl for potentially zero sizes allocations needs a corresponding default impl in dealloc that does nothing.
…On February 1, 2020 6:27:44 PM EST, Lokathor ***@***.***> wrote:
an allocator should have sentinel values for ZST inputs, and _usually_
those sentinel values should be the alignment requested.
Of course if you potentially allocate out of the first 16 bytes or
whatever you'd have to pick some other sentinels.
--
You are receiving this because you commented.
Reply to this email directly or view it on GitHub:
#16 (comment)
|
The trait is |
I think a sentinal value may be used as const value, as different allocators may have different sentinals. This sentinal value have to be checked in |
Hmm... So supporting zero sized allocation requires an extra branch condition in |
You don't need a sentinel. Just mark dealloc unsafe and do nothing if the size is zero. If the outer alloc is marked inline that'll be optimized out in most cases.
…On February 1, 2020 6:39:35 PM EST, Scott J Maddox ***@***.***> wrote:
Hmm... So supporting zero sized allocation requires an extra branch
condition in `alloc` and an arbitrary number of extra branches (or
maybe a jump table?) in `dealloc` to check for sentinel values...
Sounds kind-of expensive. Maybe supporting zero-sized allocation isn't
such a great idea...
--
You are receiving this because you commented.
Reply to this email directly or view it on GitHub:
#16 (comment)
|
(and actually, or course it's unsafe already)
…On February 1, 2020 6:39:35 PM EST, Scott J Maddox ***@***.***> wrote:
Hmm... So supporting zero sized allocation requires an extra branch
condition in `alloc` and an arbitrary number of extra branches (or
maybe a jump table?) in `dealloc` to check for sentinel values...
Sounds kind-of expensive. Maybe supporting zero-sized allocation isn't
such a great idea...
--
You are receiving this because you commented.
Reply to this email directly or view it on GitHub:
#16 (comment)
|
Oh, right, I forgot the |
The former. As #[inline(always)]
unsafe fn alloc_zero_sized_type(&self, align: usize) -> NonNull<u8> {
// We want to use NonNull::dangling here, but that function uses mem::align_of::<T>
// internally. For our use-case we cannot call dangling::<T>, since we are not generic
// over T; we only have access to the Layout of T. Instead we re-implement the
// functionality here.
//
// See https://github.com/rust-lang/rust/blob/9966af3/src/libcore/ptr/non_null.rs#L70
// for the reference implementation.
let ptr = align as *mut u8;
NonNull::new_unchecked(ptr)
}
I forgot about At this point you basically have a set of safe |
How often could you possibly be allocating that you need to worry about a single Surely the rest of the allocator isn't branchless anyway. |
The way I see it As such I'd argue for something along the lines of: trait AllocRef {
fn alloc_sized(self, layout: NonZeroLayout) -> NonNull<u8>;
/// Has default implementation
#[inline(always)]
unsafe fn alloc_zero_sized_type(self, align: usize) -> NonNull<u8> {
// We want to use NonNull::dangling here, but that function uses mem::align_of::<T>
// internally. For our use-case we cannot call dangling::<T>, since we are not generic
// over T; we only have access to the Layout of T. Instead we re-implement the
// functionality here.
//
// See https://github.com/rust-lang/rust/blob/9966af3/src/libcore/ptr/non_null.rs#L70
// for the reference implementation.
let ptr = align as *mut u8;
NonNull::new_unchecked(ptr)
}
unsafe fn alloc(self, layout: Layout) -> NonNull<u8> {
if layout.size() == 0 {
self.alloc_zero_sized_type(layout.align())
} else {
self.alloc_sized(NonZeroLayout::from_layout_unchecked(layout))
}
} Devs can choose the performant route; implementing and calling |
Optimization in a hot code path (such as an allocator) means everything. Branching is a big part of that. E.g. https://fitzgeraldnick.com/2019/11/01/always-bump-downwards.html |
Bump allocators only need a few (very fast) integer ops and a branch to check if there's enough memory available. An extra branch would be significant. |
@Wodann I think your approach is matured enough to be summed up as a proposal. Do you want to make one and file a new issue? While I don't like to have too many functions in |
Not denying this, but I would assume this branch is optimized away by the compiler most of the time, since often constant sizes are allocated. So constant folding should remove the check, assuming the check is inlined. Especially in @Wodann case. If you put #[inline] on that alloc method, then performance should be the same most of the time. |
Would a bump allocator need to explicitly check for zero to support zero-sized allocations? IIUC its (I couldn't personally come up with an allocator usage where the performance of a single branch in |
The returned pointer needs to have the proper alignment, which could require the edit: My response didn't make it clear that "branch statement" refers to the bounds check. Indeed for the bump allocator a
For any compile-time constants, that's true but not for runtime-specified layouts. Whether that is a common, I can't tell.
Yes, I can do that. What would be favorable, us adding the additional functions guaranteeing a zero-cost API for both compile-time constants and runtime variables? Or making the API simpler? |
But checking for alignment and needing to offset by some amount from the current bump location is done regardless of the allocation size, so i don't think that counts. EDIT: also most Zero-sized types are alignment 1. |
I can see it happening mostly for datastructures that want to grow their buffer. I'm not sure but in those cases the compiler might still be able to assert that it's non-zero in most cases.
I think @TimDiekmann meant your 3 method approach, where only
|
I like the NonZeroUsize approach because a person can use NonZeroUsize::new_unchecked if they want to on their end, and then that keeps that particular bit of unsafety outside of the allocators. |
for?
Sounds reasonable.
That's a good point. I can make
|
more than just non-zero, it has to be power-of-two. |
I checked the docs. The |
Edit: striking through my comment, as my question was based on a wrong conclusion. |
What do you mean "forbid the usage of zero"?
`size` is allowed to be zero; only `align` must be nonzero (and a power of two).
…On February 3, 2020 8:23:42 AM EST, Wodann ***@***.***> wrote:
I was preparing my proposal, but found an issue with `Layout`.
Currently, safe ways of
[initializing](https://doc.rust-lang.org/nightly/std/alloc/struct.Layout.html#method.from_size_align)
a `Layout` forbid the usage of zero. If we are saying that it is safe
to allocate a zero-sized type by including a default implementation in
`alloc`, I feel that we should also make it safe to initialize a
`Layout` with zero.
Do we want to make this required change to `Layout::from_size_align`?
--
You are receiving this because you commented.
Reply to this email directly or view it on GitHub:
#16 (comment)
|
You are so right. Sorry about that.. |
No worries. I had to read it twice myself to be sure. :)
…On February 3, 2020 8:42:35 AM EST, Wodann ***@***.***> wrote:
You are so right. Sorry about that..
--
You are receiving this because you commented.
Reply to this email directly or view it on GitHub:
#16 (comment)
|
I close this in favor of #38 |
From rust-lang/rust#32838 (comment):
Making
Layout
only accept non-zero size is not possible anymore as it's stable. But banning zero-size allocation would simplify the safety clause onAlloc
.This would also unlock to move helper methods to an extension trait.
This change could easily be reverted in the future if actual use cases appear. Currently I don't see any, because we cannot rely on an implementation to allow zero-sized allocations, thus we have to check
size_of::<T>() == 0
in any way.The text was updated successfully, but these errors were encountered: