Skip to content

Commit cf8d98b

Browse files
committed
Auto merge of rust-lang#108623 - scottmcm:try-different-as-slice-impl, r=the8472
Move `Option::as_slice` to an always-sound implementation This approach depends on CSE to not have any branches or selects when the guessed offset is correct -- which it always will be right now -- but to also be *sound* (just less efficient) if the layout algorithms change such that the guess is incorrect. The codegen test confirms that CSE handles this as expected, leaving the optimal codegen. cc JakobDegen rust-lang#108545
2 parents f1b1ed7 + e975057 commit cf8d98b

File tree

2 files changed

+96
-42
lines changed

2 files changed

+96
-42
lines changed

library/core/src/option.rs

+83-37
Original file line numberDiff line numberDiff line change
@@ -735,22 +735,47 @@ impl<T> Option<T> {
735735
}
736736
}
737737

738-
const fn get_some_offset() -> isize {
739-
if mem::size_of::<Option<T>>() == mem::size_of::<T>() {
740-
// niche optimization means the `T` is always stored at the same position as the Option.
741-
0
738+
/// This is a guess at how many bytes into the option the payload can be found.
739+
///
740+
/// For niche-optimized types it's correct because it's pigeon-holed to only
741+
/// one possible place. For other types, it's usually correct today, but
742+
/// tweaks to the layout algorithm (particularly expansions of
743+
/// `-Z randomize-layout`) might make it incorrect at any point.
744+
///
745+
/// It's guaranteed to be a multiple of alignment (so will always give a
746+
/// correctly-aligned location) and to be within the allocated object, so
747+
/// is valid to use with `offset` and to use for a zero-sized read.
748+
///
749+
/// FIXME: This is a horrible hack, but allows a nice optimization. It should
750+
/// be replaced with `offset_of!` once that works on enum variants.
751+
const SOME_BYTE_OFFSET_GUESS: isize = {
752+
let some_uninit = Some(mem::MaybeUninit::<T>::uninit());
753+
let payload_ref = some_uninit.as_ref().unwrap();
754+
// SAFETY: `as_ref` gives an address inside the existing `Option`,
755+
// so both pointers are derived from the same thing and the result
756+
// cannot overflow an `isize`.
757+
let offset = unsafe { <*const _>::byte_offset_from(payload_ref, &some_uninit) };
758+
759+
// The offset is into the object, so it's guaranteed to be non-negative.
760+
assert!(offset >= 0);
761+
762+
// The payload and the overall option are aligned,
763+
// so the offset will be a multiple of the alignment too.
764+
assert!((offset as usize) % mem::align_of::<T>() == 0);
765+
766+
let max_offset = mem::size_of::<Self>() - mem::size_of::<T>();
767+
if offset as usize <= max_offset {
768+
// There's enough space after this offset for a `T` to exist without
769+
// overflowing the bounds of the object, so let's try it.
770+
offset
742771
} else {
743-
assert!(mem::size_of::<Option<T>>() == mem::size_of::<Option<mem::MaybeUninit<T>>>());
744-
let some_uninit = Some(mem::MaybeUninit::<T>::uninit());
745-
// SAFETY: This gets the byte offset of the `Some(_)` value following the fact that
746-
// niche optimization is not active, and thus Option<T> and Option<MaybeUninit<t>> share
747-
// the same layout.
748-
unsafe {
749-
(some_uninit.as_ref().unwrap() as *const mem::MaybeUninit<T>)
750-
.byte_offset_from(&some_uninit as *const Option<mem::MaybeUninit<T>>)
751-
}
772+
// The offset guess is definitely wrong, so use the address
773+
// of the original option since we have it already.
774+
// This also correctly handles the case of layout-optimized enums
775+
// where `max_offset == 0` and thus this is the only possibility.
776+
0
752777
}
753-
}
778+
};
754779

755780
/// Returns a slice of the contained value, if any. If this is `None`, an
756781
/// empty slice is returned. This can be useful to have a single type of
@@ -784,18 +809,28 @@ impl<T> Option<T> {
784809
#[must_use]
785810
#[unstable(feature = "option_as_slice", issue = "108545")]
786811
pub fn as_slice(&self) -> &[T] {
787-
// SAFETY: This is sound as long as `get_some_offset` returns the
788-
// correct offset. Though in the `None` case, the slice may be located
789-
// at a pointer pointing into padding, the fact that the slice is
790-
// empty, and the padding is at a properly aligned position for a
791-
// value of that type makes it sound.
792-
unsafe {
793-
slice::from_raw_parts(
794-
(self as *const Option<T>).wrapping_byte_offset(Self::get_some_offset())
795-
as *const T,
796-
self.is_some() as usize,
797-
)
798-
}
812+
let payload_ptr: *const T =
813+
// The goal here is that both arms here are calculating exactly
814+
// the same pointer, and thus it'll be folded away when the guessed
815+
// offset is correct, but if the guess is wrong for some reason
816+
// it'll at least still be sound, just no longer optimal.
817+
if let Some(payload) = self {
818+
payload
819+
} else {
820+
let self_ptr: *const Self = self;
821+
// SAFETY: `SOME_BYTE_OFFSET_GUESS` guarantees that its value is
822+
// such that this will be in-bounds of the object.
823+
unsafe { self_ptr.byte_offset(Self::SOME_BYTE_OFFSET_GUESS).cast() }
824+
};
825+
let len = usize::from(self.is_some());
826+
827+
// SAFETY: When the `Option` is `Some`, we're using the actual pointer
828+
// to the payload, with a length of 1, so this is equivalent to
829+
// `slice::from_ref`, and thus is safe.
830+
// When the `Option` is `None`, the length used is 0, so to be safe it
831+
// just needs to be aligned, which it is because `&self` is aligned and
832+
// the offset used is a multiple of alignment.
833+
unsafe { slice::from_raw_parts(payload_ptr, len) }
799834
}
800835

801836
/// Returns a mutable slice of the contained value, if any. If this is
@@ -840,17 +875,28 @@ impl<T> Option<T> {
840875
#[must_use]
841876
#[unstable(feature = "option_as_slice", issue = "108545")]
842877
pub fn as_mut_slice(&mut self) -> &mut [T] {
843-
// SAFETY: This is sound as long as `get_some_offset` returns the
844-
// correct offset. Though in the `None` case, the slice may be located
845-
// at a pointer pointing into padding, the fact that the slice is
846-
// empty, and the padding is at a properly aligned position for a
847-
// value of that type makes it sound.
848-
unsafe {
849-
slice::from_raw_parts_mut(
850-
(self as *mut Option<T>).wrapping_byte_offset(Self::get_some_offset()) as *mut T,
851-
self.is_some() as usize,
852-
)
853-
}
878+
let payload_ptr: *mut T =
879+
// The goal here is that both arms here are calculating exactly
880+
// the same pointer, and thus it'll be folded away when the guessed
881+
// offset is correct, but if the guess is wrong for some reason
882+
// it'll at least still be sound, just no longer optimal.
883+
if let Some(payload) = self {
884+
payload
885+
} else {
886+
let self_ptr: *mut Self = self;
887+
// SAFETY: `SOME_BYTE_OFFSET_GUESS` guarantees that its value is
888+
// such that this will be in-bounds of the object.
889+
unsafe { self_ptr.byte_offset(Self::SOME_BYTE_OFFSET_GUESS).cast() }
890+
};
891+
let len = usize::from(self.is_some());
892+
893+
// SAFETY: When the `Option` is `Some`, we're using the actual pointer
894+
// to the payload, with a length of 1, so this is equivalent to
895+
// `slice::from_mut`, and thus is safe.
896+
// When the `Option` is `None`, the length used is 0, so to be safe it
897+
// just needs to be aligned, which it is because `&self` is aligned and
898+
// the offset used is a multiple of alignment.
899+
unsafe { slice::from_raw_parts_mut(payload_ptr, len) }
854900
}
855901

856902
/////////////////////////////////////////////////////////////////////////

tests/codegen/option-as-slice.rs

+13-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// compile-flags: -O
1+
// compile-flags: -O -Z randomize-layout=no
22
// only-x86_64
33

44
#![crate_type = "lib"]
@@ -12,17 +12,25 @@ use core::option::Option;
1212
// CHECK-LABEL: @u64_opt_as_slice
1313
#[no_mangle]
1414
pub fn u64_opt_as_slice(o: &Option<u64>) -> &[u64] {
15-
// CHECK: start:
1615
// CHECK-NOT: select
17-
// CHECK: ret
16+
// CHECK-NOT: br
17+
// CHECK-NOT: switch
18+
// CHECK-NOT: icmp
1819
o.as_slice()
1920
}
2021

2122
// CHECK-LABEL: @nonzero_u64_opt_as_slice
2223
#[no_mangle]
2324
pub fn nonzero_u64_opt_as_slice(o: &Option<NonZeroU64>) -> &[NonZeroU64] {
24-
// CHECK: start:
2525
// CHECK-NOT: select
26-
// CHECK: ret
26+
// CHECK-NOT: br
27+
// CHECK-NOT: switch
28+
// CHECK-NOT: icmp
29+
// CHECK: %[[NZ:.+]] = icmp ne i64 %{{.+}}, 0
30+
// CHECK-NEXT: zext i1 %[[NZ]] to i64
31+
// CHECK-NOT: select
32+
// CHECK-NOT: br
33+
// CHECK-NOT: switch
34+
// CHECK-NOT: icmp
2735
o.as_slice()
2836
}

0 commit comments

Comments
 (0)