Skip to content

Commit e4b9f86

Browse files
committed
Auto merge of #109035 - scottmcm:ptr-read-should-know-undef, r=WaffleLapkin,JakobDegen
Ensure `ptr::read` gets all the same LLVM `load` metadata that dereferencing does I was looking into `array::IntoIter` optimization, and noticed that it wasn't annotating the loads with `noundef` for simple things like `array::IntoIter<i32, N>`. Trying to narrow it down, it seems that was because `MaybeUninit::assume_init_read` isn't marking the load as initialized (<https://rust.godbolt.org/z/Mxd8TPTnv>), which is unfortunate since that's basically its reason to exist. The root cause is that `ptr::read` is currently implemented via the *untyped* `copy_nonoverlapping`, and thus the `load` doesn't get any type-aware metadata: no `noundef`, no `!range`. This PR solves that by lowering `ptr::read(p)` to `copy *p` in MIR, for which the backends already do the right thing. Fortuitiously, this also improves the IR we give to LLVM for things like `mem::replace`, and fixes a couple of long-standing bugs where `ptr::read` on `Copy` types was worse than `*`ing them. Zulip conversation: <https://rust-lang.zulipchat.com/#narrow/stream/219381-t-libs/topic/Move.20array.3A.3AIntoIter.20to.20ManuallyDrop/near/341189936> cc `@erikdesjardins` `@JakobDegen` `@workingjubilee` `@the8472` Fixes #106369 Fixes #73258
2 parents 992d154 + dfc3377 commit e4b9f86

17 files changed

+358
-48
lines changed

Diff for: compiler/rustc_hir_analysis/src/check/intrinsic.rs

+2
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,8 @@ pub fn check_intrinsic_type(tcx: TyCtxt<'_>, it: &hir::ForeignItem<'_>) {
363363
sym::likely => (0, vec![tcx.types.bool], tcx.types.bool),
364364
sym::unlikely => (0, vec![tcx.types.bool], tcx.types.bool),
365365

366+
sym::read_via_copy => (1, vec![tcx.mk_imm_ptr(param(0))], param(0)),
367+
366368
sym::discriminant_value => {
367369
let assoc_items = tcx.associated_item_def_ids(
368370
tcx.require_lang_item(hir::LangItem::DiscriminantKind, None),

Diff for: compiler/rustc_lint_defs/src/builtin.rs

+4-3
Original file line numberDiff line numberDiff line change
@@ -1026,12 +1026,13 @@ declare_lint! {
10261026
/// ### Example
10271027
///
10281028
/// ```rust,compile_fail
1029-
/// #![feature(const_ptr_read)]
1029+
/// #![feature(const_mut_refs)]
10301030
/// const FOO: () = unsafe {
10311031
/// let x = &[0_u8; 4];
10321032
/// let y = x.as_ptr().cast::<u32>();
1033-
/// y.read(); // the address of a `u8` array is unknown and thus we don't know if
1034-
/// // it is aligned enough for reading a `u32`.
1033+
/// let mut z = 123;
1034+
/// y.copy_to_nonoverlapping(&mut z, 1); // the address of a `u8` array is unknown
1035+
/// // and thus we don't know if it is aligned enough for copying a `u32`.
10351036
/// };
10361037
/// ```
10371038
///

Diff for: compiler/rustc_mir_transform/src/lower_intrinsics.rs

+29
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,35 @@ impl<'tcx> MirPass<'tcx> for LowerIntrinsics {
149149
terminator.kind = TerminatorKind::Goto { target };
150150
}
151151
}
152+
sym::read_via_copy => {
153+
let [arg] = args.as_slice() else {
154+
span_bug!(terminator.source_info.span, "Wrong number of arguments");
155+
};
156+
let derefed_place =
157+
if let Some(place) = arg.place() && let Some(local) = place.as_local() {
158+
tcx.mk_place_deref(local.into())
159+
} else {
160+
span_bug!(terminator.source_info.span, "Only passing a local is supported");
161+
};
162+
terminator.kind = match *target {
163+
None => {
164+
// No target means this read something uninhabited,
165+
// so it must be unreachable, and we don't need to
166+
// preserve the assignment either.
167+
TerminatorKind::Unreachable
168+
}
169+
Some(target) => {
170+
block.statements.push(Statement {
171+
source_info: terminator.source_info,
172+
kind: StatementKind::Assign(Box::new((
173+
*destination,
174+
Rvalue::Use(Operand::Copy(derefed_place)),
175+
))),
176+
});
177+
TerminatorKind::Goto { target }
178+
}
179+
}
180+
}
152181
sym::discriminant_value => {
153182
if let (Some(target), Some(arg)) = (*target, args[0].place()) {
154183
let arg = tcx.mk_place_deref(arg);

Diff for: compiler/rustc_span/src/symbol.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1153,6 +1153,7 @@ symbols! {
11531153
read_enum_variant_arg,
11541154
read_struct,
11551155
read_struct_field,
1156+
read_via_copy,
11561157
readonly,
11571158
realloc,
11581159
reason,

Diff for: library/core/src/intrinsics.rs

+10
Original file line numberDiff line numberDiff line change
@@ -2020,6 +2020,16 @@ extern "rust-intrinsic" {
20202020
#[rustc_safe_intrinsic]
20212021
pub fn saturating_sub<T: Copy>(a: T, b: T) -> T;
20222022

2023+
/// This is an implementation detail of [`crate::ptr::read`] and should
2024+
/// not be used anywhere else. See its comments for why this exists.
2025+
///
2026+
/// This intrinsic can *only* be called where the argument is a local without
2027+
/// projections (`read_via_copy(p)`, not `read_via_copy(*p)`) so that it
2028+
/// trivially obeys runtime-MIR rules about derefs in operands.
2029+
#[cfg(not(bootstrap))]
2030+
#[rustc_const_unstable(feature = "const_ptr_read", issue = "80377")]
2031+
pub fn read_via_copy<T>(p: *const T) -> T;
2032+
20232033
/// Returns the value of the discriminant for the variant in 'v';
20242034
/// if `T` has no discriminant, returns `0`.
20252035
///

Diff for: library/core/src/ptr/mod.rs

+45-14
Original file line numberDiff line numberDiff line change
@@ -1135,27 +1135,58 @@ pub const unsafe fn replace<T>(dst: *mut T, mut src: T) -> T {
11351135
#[rustc_const_unstable(feature = "const_ptr_read", issue = "80377")]
11361136
#[cfg_attr(miri, track_caller)] // even without panics, this helps for Miri backtraces
11371137
pub const unsafe fn read<T>(src: *const T) -> T {
1138-
// We are calling the intrinsics directly to avoid function calls in the generated code
1139-
// as `intrinsics::copy_nonoverlapping` is a wrapper function.
1140-
extern "rust-intrinsic" {
1141-
#[rustc_const_stable(feature = "const_intrinsic_copy", since = "1.63.0")]
1142-
fn copy_nonoverlapping<T>(src: *const T, dst: *mut T, count: usize);
1143-
}
1138+
// It would be semantically correct to implement this via `copy_nonoverlapping`
1139+
// and `MaybeUninit`, as was done before PR #109035. Calling `assume_init`
1140+
// provides enough information to know that this is a typed operation.
11441141

1145-
let mut tmp = MaybeUninit::<T>::uninit();
1146-
// SAFETY: the caller must guarantee that `src` is valid for reads.
1147-
// `src` cannot overlap `tmp` because `tmp` was just allocated on
1148-
// the stack as a separate allocated object.
1142+
// However, as of March 2023 the compiler was not capable of taking advantage
1143+
// of that information. Thus the implementation here switched to an intrinsic,
1144+
// which lowers to `_0 = *src` in MIR, to address a few issues:
11491145
//
1150-
// Also, since we just wrote a valid value into `tmp`, it is guaranteed
1151-
// to be properly initialized.
1146+
// - Using `MaybeUninit::assume_init` after a `copy_nonoverlapping` was not
1147+
// turning the untyped copy into a typed load. As such, the generated
1148+
// `load` in LLVM didn't get various metadata, such as `!range` (#73258),
1149+
// `!nonnull`, and `!noundef`, resulting in poorer optimization.
1150+
// - Going through the extra local resulted in multiple extra copies, even
1151+
// in optimized MIR. (Ignoring StorageLive/Dead, the intrinsic is one
1152+
// MIR statement, while the previous implementation was eight.) LLVM
1153+
// could sometimes optimize them away, but because `read` is at the core
1154+
// of so many things, not having them in the first place improves what we
1155+
// hand off to the backend. For example, `mem::replace::<Big>` previously
1156+
// emitted 4 `alloca` and 6 `memcpy`s, but is now 1 `alloc` and 3 `memcpy`s.
1157+
// - In general, this approach keeps us from getting any more bugs (like
1158+
// #106369) that boil down to "`read(p)` is worse than `*p`", as this
1159+
// makes them look identical to the backend (or other MIR consumers).
1160+
//
1161+
// Future enhancements to MIR optimizations might well allow this to return
1162+
// to the previous implementation, rather than using an intrinsic.
1163+
1164+
// SAFETY: the caller must guarantee that `src` is valid for reads.
11521165
unsafe {
11531166
assert_unsafe_precondition!(
11541167
"ptr::read requires that the pointer argument is aligned and non-null",
11551168
[T](src: *const T) => is_aligned_and_not_null(src)
11561169
);
1157-
copy_nonoverlapping(src, tmp.as_mut_ptr(), 1);
1158-
tmp.assume_init()
1170+
1171+
#[cfg(bootstrap)]
1172+
{
1173+
// We are calling the intrinsics directly to avoid function calls in the
1174+
// generated code as `intrinsics::copy_nonoverlapping` is a wrapper function.
1175+
extern "rust-intrinsic" {
1176+
#[rustc_const_stable(feature = "const_intrinsic_copy", since = "1.63.0")]
1177+
fn copy_nonoverlapping<T>(src: *const T, dst: *mut T, count: usize);
1178+
}
1179+
1180+
// `src` cannot overlap `tmp` because `tmp` was just allocated on
1181+
// the stack as a separate allocated object.
1182+
let mut tmp = MaybeUninit::<T>::uninit();
1183+
copy_nonoverlapping(src, tmp.as_mut_ptr(), 1);
1184+
tmp.assume_init()
1185+
}
1186+
#[cfg(not(bootstrap))]
1187+
{
1188+
crate::intrinsics::read_via_copy(src)
1189+
}
11591190
}
11601191
}
11611192

Diff for: tests/codegen/issues/issue-106369.rs

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// compile-flags: -O
2+
// ignore-debug (the extra assertions get in the way)
3+
4+
#![crate_type = "lib"]
5+
6+
// From <https://github.com/rust-lang/rust/issues/106369#issuecomment-1369095304>
7+
8+
// CHECK-LABEL: @issue_106369(
9+
#[no_mangle]
10+
pub unsafe fn issue_106369(ptr: *const &i32) -> bool {
11+
// CHECK-NOT: icmp
12+
// CHECK: ret i1 true
13+
// CHECK-NOT: icmp
14+
Some(std::ptr::read(ptr)).is_some()
15+
}

Diff for: tests/codegen/issues/issue-73258.rs

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// compile-flags: -O
2+
// ignore-debug (the extra assertions get in the way)
3+
4+
#![crate_type = "lib"]
5+
6+
// Adapted from <https://github.com/rust-lang/rust/issues/73258#issue-637346014>
7+
8+
#[derive(Clone, Copy)]
9+
#[repr(u8)]
10+
pub enum Foo {
11+
A, B, C, D,
12+
}
13+
14+
// CHECK-LABEL: @issue_73258(
15+
#[no_mangle]
16+
pub unsafe fn issue_73258(ptr: *const Foo) -> Foo {
17+
// CHECK-NOT: icmp
18+
// CHECK-NOT: call
19+
// CHECK-NOT: br
20+
// CHECK-NOT: select
21+
22+
// CHECK: %[[R:.+]] = load i8
23+
// CHECK-SAME: !range !
24+
25+
// CHECK-NOT: icmp
26+
// CHECK-NOT: call
27+
// CHECK-NOT: br
28+
// CHECK-NOT: select
29+
30+
// CHECK: ret i8 %[[R]]
31+
32+
// CHECK-NOT: icmp
33+
// CHECK-NOT: call
34+
// CHECK-NOT: br
35+
// CHECK-NOT: select
36+
let k: Option<Foo> = Some(ptr.read());
37+
return k.unwrap();
38+
}

Diff for: tests/codegen/mem-replace-big-type.rs

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// This test ensures that `mem::replace::<T>` only ever calls `@llvm.memcpy`
2+
// with `size_of::<T>()` as the size, and never goes through any wrapper that
3+
// may e.g. multiply `size_of::<T>()` with a variable "count" (which is only
4+
// known to be `1` after inlining).
5+
6+
// compile-flags: -C no-prepopulate-passes -Zinline-mir=no
7+
// ignore-debug: the debug assertions get in the way
8+
9+
#![crate_type = "lib"]
10+
11+
#[repr(C, align(8))]
12+
pub struct Big([u64; 7]);
13+
pub fn replace_big(dst: &mut Big, src: Big) -> Big {
14+
// Before the `read_via_copy` intrinsic, this emitted six `memcpy`s.
15+
std::mem::replace(dst, src)
16+
}
17+
18+
// NOTE(eddyb) the `CHECK-NOT`s ensure that the only calls of `@llvm.memcpy` in
19+
// the entire output, are the direct calls we want, from `ptr::replace`.
20+
21+
// CHECK-NOT: call void @llvm.memcpy
22+
23+
// For a large type, we expect exactly three `memcpy`s
24+
// CHECK-LABEL: define internal void @{{.+}}mem{{.+}}replace{{.+}}sret(%Big)
25+
// CHECK-NOT: alloca
26+
// CHECK: alloca %Big
27+
// CHECK-NOT: alloca
28+
// CHECK-NOT: call void @llvm.memcpy
29+
// CHECK: call void @llvm.memcpy.{{.+}}({{i8\*|ptr}} align 8 %{{.*}}, {{i8\*|ptr}} align 8 %{{.*}}, i{{.*}} 56, i1 false)
30+
// CHECK-NOT: call void @llvm.memcpy
31+
// CHECK: call void @llvm.memcpy.{{.+}}({{i8\*|ptr}} align 8 %{{.*}}, {{i8\*|ptr}} align 8 %{{.*}}, i{{.*}} 56, i1 false)
32+
// CHECK-NOT: call void @llvm.memcpy
33+
// CHECK: call void @llvm.memcpy.{{.+}}({{i8\*|ptr}} align 8 %{{.*}}, {{i8\*|ptr}} align 8 %{{.*}}, i{{.*}} 56, i1 false)
34+
// CHECK-NOT: call void @llvm.memcpy
35+
36+
// CHECK-NOT: call void @llvm.memcpy

Diff for: tests/codegen/mem-replace-direct-memcpy.rs

+15-6
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,21 @@ pub fn replace_byte(dst: &mut u8, src: u8) -> u8 {
1313
}
1414

1515
// NOTE(eddyb) the `CHECK-NOT`s ensure that the only calls of `@llvm.memcpy` in
16-
// the entire output, are the two direct calls we want, from `ptr::replace`.
16+
// the entire output, are the direct calls we want, from `ptr::replace`.
1717

1818
// CHECK-NOT: call void @llvm.memcpy
19-
// CHECK: ; core::mem::replace
20-
// CHECK-NOT: call void @llvm.memcpy
21-
// CHECK: call void @llvm.memcpy.{{.+}}({{i8\*|ptr}} align 1 %{{.*}}, {{i8\*|ptr}} align 1 %{{.*}}, i{{.*}} 1, i1 false)
22-
// CHECK-NOT: call void @llvm.memcpy
23-
// CHECK: call void @llvm.memcpy.{{.+}}({{i8\*|ptr}} align 1 %{{.*}}, {{i8\*|ptr}} align 1 %{{.*}}, i{{.*}} 1, i1 false)
19+
20+
// For a small type, we expect one each of `load`/`store`/`memcpy` instead
21+
// CHECK-LABEL: define internal noundef i8 @{{.+}}mem{{.+}}replace
22+
// CHECK-NOT: alloca
23+
// CHECK: alloca i8
24+
// CHECK-NOT: alloca
25+
// CHECK-NOT: call void @llvm.memcpy
26+
// CHECK: load i8
27+
// CHECK-NOT: call void @llvm.memcpy
28+
// CHECK: store i8
29+
// CHECK-NOT: call void @llvm.memcpy
30+
// CHECK: call void @llvm.memcpy.{{.+}}({{i8\*|ptr}} align 1 %{{.*}}, {{i8\*|ptr}} align 1 %{{.*}}, i{{.*}} 1, i1 false)
31+
// CHECK-NOT: call void @llvm.memcpy
32+
2433
// CHECK-NOT: call void @llvm.memcpy

Diff for: tests/codegen/ptr-read-metadata.rs

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// compile-flags: -O -Z merge-functions=disabled
2+
// no-system-llvm
3+
// ignore-debug (the extra assertions get in the way)
4+
5+
#![crate_type = "lib"]
6+
7+
// Ensure that various forms of reading pointers correctly annotate the `load`s
8+
// with `!noundef` and `!range` metadata to enable extra optimization.
9+
10+
use std::mem::MaybeUninit;
11+
12+
// CHECK-LABEL: define noundef i8 @copy_byte(
13+
#[no_mangle]
14+
pub unsafe fn copy_byte(p: *const u8) -> u8 {
15+
// CHECK-NOT: load
16+
// CHECK: load i8, ptr %p, align 1
17+
// CHECK-SAME: !noundef !
18+
// CHECK-NOT: load
19+
*p
20+
}
21+
22+
// CHECK-LABEL: define noundef i8 @read_byte(
23+
#[no_mangle]
24+
pub unsafe fn read_byte(p: *const u8) -> u8 {
25+
// CHECK-NOT: load
26+
// CHECK: load i8, ptr %p, align 1
27+
// CHECK-SAME: !noundef !
28+
// CHECK-NOT: load
29+
p.read()
30+
}
31+
32+
// CHECK-LABEL: define i8 @read_byte_maybe_uninit(
33+
#[no_mangle]
34+
pub unsafe fn read_byte_maybe_uninit(p: *const MaybeUninit<u8>) -> MaybeUninit<u8> {
35+
// CHECK-NOT: load
36+
// CHECK: load i8, ptr %p, align 1
37+
// CHECK-NOT: noundef
38+
// CHECK-NOT: load
39+
p.read()
40+
}
41+
42+
// CHECK-LABEL: define noundef i8 @read_byte_assume_init(
43+
#[no_mangle]
44+
pub unsafe fn read_byte_assume_init(p: &MaybeUninit<u8>) -> u8 {
45+
// CHECK-NOT: load
46+
// CHECK: load i8, ptr %p, align 1
47+
// CHECK-SAME: !noundef !
48+
// CHECK-NOT: load
49+
p.assume_init_read()
50+
}
51+
52+
// CHECK-LABEL: define noundef i32 @copy_char(
53+
#[no_mangle]
54+
pub unsafe fn copy_char(p: *const char) -> char {
55+
// CHECK-NOT: load
56+
// CHECK: load i32, ptr %p
57+
// CHECK-SAME: !range ![[RANGE:[0-9]+]]
58+
// CHECK-SAME: !noundef !
59+
// CHECK-NOT: load
60+
*p
61+
}
62+
63+
// CHECK-LABEL: define noundef i32 @read_char(
64+
#[no_mangle]
65+
pub unsafe fn read_char(p: *const char) -> char {
66+
// CHECK-NOT: load
67+
// CHECK: load i32, ptr %p
68+
// CHECK-SAME: !range ![[RANGE]]
69+
// CHECK-SAME: !noundef !
70+
// CHECK-NOT: load
71+
p.read()
72+
}
73+
74+
// CHECK-LABEL: define i32 @read_char_maybe_uninit(
75+
#[no_mangle]
76+
pub unsafe fn read_char_maybe_uninit(p: *const MaybeUninit<char>) -> MaybeUninit<char> {
77+
// CHECK-NOT: load
78+
// CHECK: load i32, ptr %p
79+
// CHECK-NOT: range
80+
// CHECK-NOT: noundef
81+
// CHECK-NOT: load
82+
p.read()
83+
}
84+
85+
// CHECK-LABEL: define noundef i32 @read_char_assume_init(
86+
#[no_mangle]
87+
pub unsafe fn read_char_assume_init(p: &MaybeUninit<char>) -> char {
88+
// CHECK-NOT: load
89+
// CHECK: load i32, ptr %p
90+
// CHECK-SAME: !range ![[RANGE]]
91+
// CHECK-SAME: !noundef !
92+
// CHECK-NOT: load
93+
p.assume_init_read()
94+
}
95+
96+
// CHECK: ![[RANGE]] = !{i32 0, i32 1114112}

0 commit comments

Comments
 (0)