Rust: Tracking issue for RFC 2043: Add `align_offset` intrinsic (formerly: and `[T]::align_to` function)

Created on 11 Sep 2017  Â·  48Comments  Â·  Source: rust-lang/rust

This is a tracking issue for the RFC "Add align_offset intrinsic and [T]::align_to function " (rust-lang/rfcs#2043). align_to is stable, so this tracks just align_offset now.

Steps:

Unresolved questions:

  • [ ] "produce a lint in case sizeof<T>() % sizeof<U>() != 0 and in case the expansion is not part of a monomorphisation, since in that case align_to is statically known to never be effective
B-RFC-approved C-tracking-issue T-libs disposition-merge finished-final-comment-period

Most helpful comment

I’d like to suggest to focus on stabilising [T]::align_to if we are not sure whether align_offset is a right API for us. [T]::align_to seems to me like something that has provably the ideal API for what it does and covers majority of the use cases of the bare-bones align_offset.

The fact that it took me (with a help from another person) months to implement align_to has something to say about non-triviality of such function.

All 48 comments

@oli-obk Is it useful to stabilize align_offset before align_to is implemented?

Generally yes, because that's the key feature. Align_to is just convenience

Alright, then. Let’s stabilize align_offset and leave this issue open for tracking align_to.

@rfcbot fcp merge

Team member @SimonSapin has proposed to merge this. The next step is review by the rest of the tagged team members:

  • [x] @Kimundi
  • [x] @SimonSapin
  • [ ] @alexcrichton
  • [x] @aturon
  • [x] @dtolnay
  • [x] @sfackler
  • [ ] @withoutboats

Concerns:

Once a majority of reviewers approve (and none object), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up!

See this document for info about what commands tagged team members can give me.

@rfcbot concern motivation-and-ergonomics

The original motivation for these functions (unless it's changed, which it may have!) I believe was for miri and const evaluating various functions. Is that still the main motivation for these functions? If so do we expect that more things to be available on stable once these are stabilized?

I'm a little unclear about how and when such an intrinsic would be used so I'm mostly looking to straighten up my own thinking! In particular the clause "If it is not possible to align the pointer, the implementation returns usize::max_value()." I'm having trouble wrapping my head around in how it'd be expected to get used.

Is the long term plan to make this a const fn but otherwise disallow operations like *mut T as usize in a const fn?

In terms of ergonomics I'd also naively expect a signature like:

pub fn align_to(self, align: usize) -> Option<*mut Self>;

but would that create the same problems it's trying to avoid?

Is that still the main motivation for these functions?

yes

If so do we expect that more things to be available on stable once these are stabilized?

This will allow making it const fn, which is a separate topic in the future

I'm having trouble wrapping my head around in how it'd be expected to get used.

No matter which computation you use to figure out the number of bytes that you need to offset your pointer in order to make it aligned, you will need to check whether it still points into your buffer. If you have a [u8; 3] and have a pointer to the first element, how many bytes to do you need to offset in order to get a 8 byte aligned pointer? that might be beyond the range. So you always need to check. Returning max_value means that you can't align it in any buffer.

Is the long term plan to make this a const fn but otherwise disallow operations like *mut T as usize in a const fn?

No, these operations are fine. The issue is that no matter how many bytes you offset a *mut u8 in miri, it will never become aligned to anything with a higher alignment. This function exists to abstract away such manual pointer alignments.

In terms of ergonomics I'd also naively expect a signature like:

You could do that, but then you'd also need an argument for the allocation size. Now that I think about it, that doesn't seem too bad and since you need to check anyway... As long as the intrinsic doesn't have to return an Option (the intrinsic an impl detail anyway).

Ok, thanks for the info! It sounds like this function doesn't necessarily unlock anything in the near term as const-things are still pretty unstable? If that's the case I'm personally tempted to leave these as unstable until we've got the const story more fleshed out to see how they fit into the grand scheme of things

Oh dear, it has been almost a year since the RFC and it went totally outside my radar, until I really, really needed [T]::align_to for my current use case of… aligning a byte buffer :)

Thinking about possible implementation approaches, I realised that to implement [T]::align_to in some cases it would be necessary to resort to calling the intrinsic multiple times in a manner similar to this:

loop {
    x = intrinsics::align_offset(ptr, align);
    if (x is aligned to object start) { return }
    ptr = x.offset(1);
}

So with @oli-obk we decided that it would probably be most prudent to simply change the intrinsic to do such calculation by itself, changing its signature from the current one to align_offset<T>(*mut T, usize) -> *mut T, that aligns the pointer to the first aligned object, not the first aligned pointer value, like it does currently.

I’m almost done with the improved implementation of the align_offset intrinsic, and I have noticed, that we currently specify that offset returned by the code is in bytes. This seems suboptimal in multiple ways.

  1. Since implementation works strictly in units of "elements", a conversion step is always necessary for when the stride of T does not equal 1;
  2. Furthermore, to implement align_to, conversion from bytes back to elements is necessary again;
  3. Cannot be used directly with ptr.offset()/ptr.offset_to without converting bytes back into "elements" again;

I propose that we change align_offset to return offset in number of elements.

I'm assuming we are talking about elements of the source slice and not the aligned slice. Sgtm

I guess I should’ve said in “number of Ts” where <*const T>::align_offset(...), but yeah.

IIRC this was in bytes in the intrinsic because the pointer to align could have a ZST pointee, and always using bytes sidesteps the questions of what to do there. (Not that I'm sure _why_ you'd want to align a *const [i32; 0] to *const [u64; 0], but you _could_.) The library could plausibly have a better API, though...

FYI implementation for align_to and changes to align_offset are at https://github.com/rust-lang/rust/pull/50319.

As another case in point that getting this code right when hand-written is hard: Spot the issue in this code.

The problem is that the inner slice is not aligned properly in all cases when it is empty.

If we had a stable align_to, they could just use that. :)

ping @llogiq

This shouldn't happen because we only call the function for larger slices. But I've been wrong in the past, so I wouldn't bet on it.

I’d like to suggest to focus on stabilising [T]::align_to if we are not sure whether align_offset is a right API for us. [T]::align_to seems to me like something that has provably the ideal API for what it does and covers majority of the use cases of the bare-bones align_offset.

The fact that it took me (with a help from another person) months to implement align_to has something to say about non-triviality of such function.

Agreed, the sooner we can get people to use this instead of hand-rolling their own, the better. :)

Just one comment on a remark in the implementation PR:

Furthermore, a promise is made that the slice containing Us will be as large as possible (contrary to the RFC) – otherwise the function is quite useless.

miri cannot guarantee this. So while the implementation should do that, users of this code should NOT rely on the U part being maximal.

I cannot think of a case where this is require for correctness anyway, is there one?

Oh yes, that is true. I’m fine with removing that guarantee from the documentation.

So, what would this take? Just open an RFC that stabilizes, and ask for FCP?

@RalfJung Yeah, I think a PR to stabilize (with a comment about why) is sufficient.

<*mut u8>::align_offset can be useful when implementing a memory allocator, to satisfy the requested layout.align(). Note however that much of the complexity of the implementation of <*mut T>::align_offset does not apply when size_of::<T>() == 1. The function is also infallible in that case.

@alexcrichton Would a separate function or method for this special case (or maybe even for usize instead of pointers) address your concern?

@SimonSapin yeah the API I mentioned above I think would be fine to add and consider stabilizing

Is it has the disposition-merge tag since 9 months, which issues needs to be resolved before solving this?

<*mut u8>::align_offset can be useful when implementing a memory allocator, to satisfy the requested layout.align().

This is exactly my use case.

There is still an outstanding concern from @alexcrichton about the ergonomics of the API.

Uh, didn't it get stabilized with https://github.com/rust-lang/rust/pull/53754 ?

EDIT: Oh, I guess this tracking issue is about align_offset only now?

Oh, I guess this tracking issue is about align_offset only now?

Yes.

@rfcbot resolve motivation-and-ergonomics

I don't want to personally be on the hook for blocking this any more

:bell: This is now entering its final comment period, as per the review above. :bell:

The final comment period, with a disposition to merge, as per the review above, is now complete.

As the automated representative of the governance process, I would like to thank the author for their work and everyone else who contributed.

The RFC will be merged soon.

What's the status of this?

In terms of process, this FCP includes align_offset from what I can tell. So I guess all this needs is a stabilization PR?

Cool, I'll look into creating that then.

It is permissible for the implementation to always return usize::max_value(). Only your algorithm's performance can depend on getting a usable offset here, not its correctness.

What is the point of this function? I'm dealing with hardware where if the pointer is not aligned, the instruction will fault. So I need to implement this function myself because this function is not guaranteed to be useful?

The point of this function that if it returns an offset that is out of bounds of your allocation, you should run your backup logic that does not depend on alignment. Imagine having a *mut [u8; 2] that you try to align to be usable as *mut u16, if you get an offset of 1 (because the original pointer was not aligned), you can't use the resulting pointer. In this case using your operation would be UB because you'd be working outside of the allocation.

Platforms like miri returning usize::max_value() is essentially just a way of signaling that you need to run the backup logic that you have to have anyway in case the alignment moves beyond your buffer

run your backup logic that does not depend on alignment.

What backup logic? As stated, in my use case it is not possible to use an unaligned value.

In that case, what happens if you get an aligned value but it's outside of your allocation? You can't make a memory location aligned, you can only offset a pointer until it is aligned. But offsetting a pointer may go beyond the allocation your pointer points to

EDIT: maybe you need to make this decision at a higher level? e.g. by using the corresponding slice method (https://doc.rust-lang.org/std/primitive.slice.html#method.align_to) ?

This function is for cases where e.g. you have a &mut [u8] that could be large, and you want to get the "aligned center" of that slice as &mut [u64] for more efficient processing. Maybe your case is different, in which case maybe this function is not for you.

But as @oli-obk said, you need backup logic anyway because the pointer you are getting might not be sufficiently aligned, and the buffer might not be large enough to find something aligned "in the middle". What do you want to do in that case? I think we need a bit more context here.

This function is for cases where e.g. you have a &mut [u8] that could be large, and you want to get the "aligned center" of that slice as &mut [u64] for more efficient processing. Maybe your case is different, in which case maybe this function is not for you.

No this is pretty much exactly the case for me. But I know that my input buffer is guaranteed to be large enough to hold the "aligned center".

Well, that is a subtly different use-case -- not what this function was designed for, and also one that Miri currently does not support. (There are trade-offs here: supporting that means being less good at detecting misaligned memory accesses.)

In particular, what do you do with the unaligned prefix and suffix? Usually they also need processing, and that's why you need fall-back code.

You could always use align_to and then assert that the center part is non-empty. Or you just do the usual "round ptr up to next multiple of alignment"; you don't actually need all the complexity in align_to (which is mostly caused by returning a result measured in units of size_of::<T>()).

Or you just do the usual "round ptr up to next multiple of alignment"; you don't actually need all the complexity in align_to

I'm talking about ptr::align_offset, which, in my mind, is exactly supposed to do the rounding up. The documentation I quoted earlier is from that function.

In particular, what do you do with the unaligned prefix and suffix? Usually they also need processing, and that's why you need fall-back code.

I'm using mmap to try to allocate naturally-aligned memory by asking for twice the size I need, computing the right range, and then munmapping the rest.

I'm using mmap to try to allocate naturally-aligned memory by asking for twice the size I need, computing the right range, and then munmapping the rest.

Yeah that's not at all the use-case for which this was designed. If you want to implement an aligned allocator based on an unaligned one, this function currently won't help you. libstd in fact also contains such code because Windows.

I'm talking about ptr::align_offset, which, in my mind, is exactly supposed to do the rounding up.

Oh but it is doing way more than that. Quoting from the docs:

The offset is expressed in number of T elements, and not bytes.

That's what makes this function complicated. It actually solves an integer modulo congruence.

@RalfJung T is u8 for me.

It seems weird to me that ptr::align_offset should not be used to align pointer values except in some special cases. It's not clear at all from the documentation that you shouldn't use this function (except for the warning that the function might not give a useful output).

I guessed as much. So? All I said is, the function is more general than your use-case, making it way more complicated than "rounding up", and its reason to exist is not what you seem to think it is (to remove some simple rounding-up code from allocators -- an extremely rare thing to write).

You are looking at a very complicated tool with subtle trade-offs, wanting to use it in its simplest possible form (ignoring 99% of its functionality) and then complain that it doesn't exactly do what you want it to do. Well, I am afraid the tradeoffs that informed its design are different than yours. Probably this is not the tool you are looking for.

Assuming allocation is expensive, it also shouldn't be too much of a problem to call align_offset and then assert that your result is < align.

It's not clear at all from the documentation that you shouldn't use this function

You (like, the general public) should use it. You (@jethrogb) are just misinterpreting what it should be used for.

In fact the docs say quite clearly:

Only your algorithm's performance can depend on getting a usable offset here, not its correctness.

So, if that's not a constraint you can live with, that's okay -- just don't use this function.

Assuming allocation is expensive, it also shouldn't be too much of a problem to call align_offset and then assert that your result is < align.

Except that won't work, because:

It is permissible for the implementation to always return usize::max_value().

So when I do the assertion (which I'd be fine with), the implementation is free to unconditionally insert an assertion failure. Unless you think that that's not the case, in which case this is a documentation issue.

Only your algorithm's performance can depend on getting a usable offset here, not its correctness.

I definitely can't live with this, because now I still need to implement a "round up" function in case this function failed. Then I might as well just not use this function.

(to remove some simple rounding-up code from allocators -- an extremely rare thing to write)

I don't think pointer alignment manipulation is at all rare when dealing with low-level code, something Rust is supposed to be good at. Definitely not limited to allocators. I will not be the first person to come across this function when looking at the ptr docs thinking it would be useful.

So when I do the assertion (which I'd be fine with), the implementation is free to unconditionally insert an assertion failure.

Correct.

The current situation is that the libstd/rustc implementation will always succeed to align if size_of::<T>() == 1. However, the Miri implementation will not. The doc has to cover all implementations, but you could decide to rely on a specific one.

I don't think pointer alignment manipulation is at all rare when dealing with low-level code, something Rust is supposed to be good at. Definitely not limited to allocators. I will not be the first person to come across this function when looking at the ptr docs thinking it would be useful.

So far, allocators are the only use-case I am aware of that would need proper alignment for correctness, not just performance. Most of the time you are fed in data you have to work with; allocation is the only time when you are asked to produce memory with a certain alignment. But I am mostly unfamiliar with embedded programming and many other ways of using Rust, so if anyone reading along also has a use-case that is affected by this, please chime in. :)

For your use-case, a "please round up this pointer/integer to alignment X" function seems to be much more useful though than something that just computes the offset?

Was this page helpful?
0 / 5 - 0 ratings