What optimizations do the stronger safety requirements of the `offset` method allow?

There are two methods to offset a pointer: offset and wrapping_offset.

offset has a stronger safety requirement, specifically

Both the starting and resulting pointer must be either in bounds or one byte past the end of the same allocated object.

offset is immediate Undefined Behavior when crossing object boundaries.

The other method wrapping_offset allows us to arbitrarily offset a pointer. The difference is that

this method basically delays the requirement of staying within the same allocated object. [...] wrapping_offset produces a pointer but still leads to Undefined Behavior if a pointer is dereferenced when it is out-of-bounds of the object it is attached to.

The documentation says that the safety requirements are more conservative on offset because

offset can be optimized better and is thus preferable in performance-sensitive code.

I would like to know what specific optimizations the stronger safety requirements of the offset method allow.

3 Likes

The most important benefit is allowing the compiler to assume that the offset addition won't overflow usize and wrap the address space. In fact, there is ongoing discussion about relaxing the offset safety requirements to require only this non-wrapping behavior (removing any requirements about allocated objects).

4 Likes

What it specifically means to rustc is that we can tell LLVM that an offset is inbounds. It's also a bit easier to verify (e.g. for Miri's UB sanitization) than purely a nowrap requirement.

The most obvious interesting potential conclusion from inbounds is that if you do a read, offset, read, that the entire range is dereferenceable. However, a subtle detail is that there might not be a guarantee that allocated objects are continuous (e.g. it might be valid to have an allocated object accessible via one pointer's provenance that crosses over an unmapped page that must not be accessed), so it's unclear.

(Member of T-opsem, but this should not be construed as normative.)

4 Likes

This is the part I found most interesting, personally.

With symbolic allocations, like we basically need to do for CTFE, there's no way to check whether you wrapped the address space because we don't know where the base address of the allocation even is.

Whereas if you make a 1234-byte allocation (from let mut x = [0_u16; 617]; or whatever) then CTFE can definitely check whether the pointer stayed in-bounds of that allocation while calculating the const, and because an allocation can't wrap the address space, then it knows for sure that your offsetting inside that allocation isn't going to wrap either.

And since CTFE checks kinda prefer the same thing that LLVM wants for them too, then it might as well keep that stricter precondition. After all, there's always non-inbounds available for doing weird tricks.

3 Likes

Could we just have the requirement not apply inside const? As in, declare that code executing during CTFE can't possibly wrap the address space as there is no address space to wrap, and so never has UB due to the "don't wrap the address space" requirement?