Skip to content
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

Make mem::replace simpler in codegen #111010

Merged
merged 3 commits into from
May 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions compiler/rustc_hir_analysis/src/check/intrinsic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@ pub fn check_intrinsic_type(tcx: TyCtxt<'_>, it: &hir::ForeignItem<'_>) {
sym::unlikely => (0, vec![tcx.types.bool], tcx.types.bool),

sym::read_via_copy => (1, vec![tcx.mk_imm_ptr(param(0))], param(0)),
sym::write_via_move => (1, vec![tcx.mk_mut_ptr(param(0)), param(0)], tcx.mk_unit()),

sym::discriminant_value => {
let assoc_items = tcx.associated_item_def_ids(
Expand Down
23 changes: 23 additions & 0 deletions compiler/rustc_mir_transform/src/lower_intrinsics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,29 @@ impl<'tcx> MirPass<'tcx> for LowerIntrinsics {
}
}
}
sym::write_via_move => {
let target = target.unwrap();
let Ok([ptr, val]) = <[_; 2]>::try_from(std::mem::take(args)) else {
span_bug!(
terminator.source_info.span,
"Wrong number of arguments for write_via_move intrinsic",
);
};
let derefed_place =
if let Some(place) = ptr.place() && let Some(local) = place.as_local() {
tcx.mk_place_deref(local.into())
} else {
span_bug!(terminator.source_info.span, "Only passing a local is supported");
};
block.statements.push(Statement {
source_info: terminator.source_info,
kind: StatementKind::Assign(Box::new((
derefed_place,
Rvalue::Use(val),
))),
});
terminator.kind = TerminatorKind::Goto { target };
}
sym::discriminant_value => {
if let (Some(target), Some(arg)) = (*target, args[0].place()) {
let arg = tcx.mk_place_deref(arg);
Expand Down
1 change: 1 addition & 0 deletions compiler/rustc_span/src/symbol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1635,6 +1635,7 @@ symbols! {
write_bytes,
write_macro,
write_str,
write_via_move,
writeln_macro,
x87_reg,
xer,
Expand Down
30 changes: 27 additions & 3 deletions library/core/src/intrinsics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2257,12 +2257,23 @@ extern "rust-intrinsic" {
/// This is an implementation detail of [`crate::ptr::read`] and should
/// not be used anywhere else. See its comments for why this exists.
///
/// This intrinsic can *only* be called where the argument is a local without
/// projections (`read_via_copy(p)`, not `read_via_copy(*p)`) so that it
/// This intrinsic can *only* be called where the pointer is a local without
/// projections (`read_via_copy(ptr)`, not `read_via_copy(*ptr)`) so that it
/// trivially obeys runtime-MIR rules about derefs in operands.
#[rustc_const_unstable(feature = "const_ptr_read", issue = "80377")]
#[rustc_nounwind]
pub fn read_via_copy<T>(p: *const T) -> T;
pub fn read_via_copy<T>(ptr: *const T) -> T;

/// This is an implementation detail of [`crate::ptr::write`] and should
/// not be used anywhere else. See its comments for why this exists.
///
/// This intrinsic can *only* be called where the pointer is a local without
/// projections (`write_via_move(ptr, x)`, not `write_via_move(*ptr, x)`) so
/// that it trivially obeys runtime-MIR rules about derefs in operands.
#[cfg(not(bootstrap))]
#[rustc_const_unstable(feature = "const_ptr_write", issue = "86302")]
#[rustc_nounwind]
pub fn write_via_move<T>(ptr: *mut T, value: T);

/// Returns the value of the discriminant for the variant in 'v';
/// if `T` has no discriminant, returns `0`.
Expand Down Expand Up @@ -2828,3 +2839,16 @@ pub const unsafe fn transmute_unchecked<Src, Dst>(src: Src) -> Dst {
// SAFETY: It's a transmute -- the caller promised it's fine.
unsafe { transmute_copy(&ManuallyDrop::new(src)) }
}

/// Polyfill for bootstrap
#[cfg(bootstrap)]
pub const unsafe fn write_via_move<T>(ptr: *mut T, value: T) {
use crate::mem::*;
// SAFETY: the caller must guarantee that `dst` is valid for writes.
// `dst` cannot overlap `src` because the caller has mutable access
// to `dst` while `src` is owned by this function.
unsafe {
copy_nonoverlapping::<T>(&value, ptr, 1);
forget(value);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not important, but could you use ManuallyDrop instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I probably could, but the preexisting implementation of write went out of its way to use intrinsics::forget

intrinsics::forget(src);

even though mem::forget doesn't

pub const fn forget<T>(t: T) {
let _ = ManuallyDrop::new(t);
}

which is probably just because it has much better codegen, but in case it actually matters I'd rather just leave it like this since it sounds like you don't feel particularly strongly about it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay 👍🏻

}
}
17 changes: 8 additions & 9 deletions library/core/src/ptr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1349,13 +1349,13 @@ pub const unsafe fn read_unaligned<T>(src: *const T) -> T {
#[rustc_const_unstable(feature = "const_ptr_write", issue = "86302")]
#[cfg_attr(miri, track_caller)] // even without panics, this helps for Miri backtraces
pub const unsafe fn write<T>(dst: *mut T, src: T) {
// We are calling the intrinsics directly to avoid function calls in the generated code
// as `intrinsics::copy_nonoverlapping` is a wrapper function.
extern "rust-intrinsic" {
#[rustc_const_stable(feature = "const_intrinsic_copy", since = "1.63.0")]
#[rustc_nounwind]
fn copy_nonoverlapping<T>(src: *const T, dst: *mut T, count: usize);
}
// Semantically, it would be fine for this to be implemented as a
// `copy_nonoverlapping` and appropriate drop suppression of `src`.

// However, implementing via that currently produces more MIR than is ideal.
// Using an intrinsic keeps it down to just the simple `*dst = move src` in
// MIR (11 statements shorter, at the time of writing), and also allows
// `src` to stay an SSA value in codegen_ssa, rather than a memory one.

// SAFETY: the caller must guarantee that `dst` is valid for writes.
// `dst` cannot overlap `src` because the caller has mutable access
Expand All @@ -1365,8 +1365,7 @@ pub const unsafe fn write<T>(dst: *mut T, src: T) {
"ptr::write requires that the pointer argument is aligned and non-null",
[T](dst: *mut T) => is_aligned_and_not_null(dst)
);
copy_nonoverlapping(&src as *const T, dst, 1);
intrinsics::forget(src);
intrinsics::write_via_move(dst, src)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ fn main() {
// Also not assigning directly as that's array initialization, not assignment.
let zst_val = [1u8; 0];
unsafe { std::ptr::null_mut::<[u8; 0]>().write(zst_val) };
//~^ERROR: memory access failed: null pointer is a dangling pointer
//~^ERROR: dereferencing pointer failed: null pointer is a dangling pointer
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
error: Undefined Behavior: memory access failed: null pointer is a dangling pointer (it has no provenance)
error: Undefined Behavior: dereferencing pointer failed: null pointer is a dangling pointer (it has no provenance)
--> $DIR/null_pointer_write_zst.rs:LL:CC
|
LL | unsafe { std::ptr::null_mut::<[u8; 0]>().write(zst_val) };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ memory access failed: null pointer is a dangling pointer (it has no provenance)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ dereferencing pointer failed: null pointer is a dangling pointer (it has no provenance)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Almost seems like for nice error messages in Miri it'd be better not to lower this to an assignment. Currently it looks like *ptr = ..., so Miri sees the deref in *ptr and emits the error accordingly.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I guess I didn't think of it being a copy instead of a deref as meaningful here.

One other thing I tried was implementing write(p, x) as *p.cast() = ManuallyDrop::new(x) which I think is also a perfectly reasonable no-intrinsic implementation -- more obviously a typed write, which I think this is, given passing the parameter to the function is typed -- but would have the same "dereferencing pointer" error in MIRI.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair, I guess really this is about write implementation details where the user can't tell whether a deref happens before the write or not. I guess 'dereferencing' is not so bad here, I was just primed because of rust-lang/miri#2859.

|
= help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
= help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
Expand Down
14 changes: 6 additions & 8 deletions tests/codegen/mem-replace-big-type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@
#[repr(C, align(8))]
pub struct Big([u64; 7]);
pub fn replace_big(dst: &mut Big, src: Big) -> Big {
// Before the `read_via_copy` intrinsic, this emitted six `memcpy`s.
// Back in 1.68, this emitted six `memcpy`s.
// `read_via_copy` in 1.69 got that down to three.
// `write_via_move` has it down to just the two essential ones.
std::mem::replace(dst, src)
}

Expand All @@ -20,17 +22,13 @@ pub fn replace_big(dst: &mut Big, src: Big) -> Big {

// CHECK-NOT: call void @llvm.memcpy

// For a large type, we expect exactly three `memcpy`s
// For a large type, we expect exactly two `memcpy`s
// CHECK-LABEL: define internal void @{{.+}}mem{{.+}}replace{{.+}}sret(%Big)
// CHECK-NOT: alloca
// CHECK: alloca %Big
// CHECK-NOT: alloca
// CHECK-NOT: call void @llvm.memcpy
// CHECK: call void @llvm.memcpy.{{.+}}({{i8\*|ptr}} align 8 %{{.*}}, {{i8\*|ptr}} align 8 %{{.*}}, i{{.*}} 56, i1 false)
// CHECK-NOT: call void @llvm.memcpy
// CHECK: call void @llvm.memcpy.{{.+}}({{i8\*|ptr}} align 8 %{{.*}}, {{i8\*|ptr}} align 8 %{{.*}}, i{{.*}} 56, i1 false)
// CHECK: call void @llvm.memcpy.{{.+}}({{i8\*|ptr}} align 8 %0, {{i8\*|ptr}} align 8 %dest, i{{.*}} 56, i1 false)
// CHECK-NOT: call void @llvm.memcpy
// CHECK: call void @llvm.memcpy.{{.+}}({{i8\*|ptr}} align 8 %{{.*}}, {{i8\*|ptr}} align 8 %{{.*}}, i{{.*}} 56, i1 false)
// CHECK: call void @llvm.memcpy.{{.+}}({{i8\*|ptr}} align 8 %dest, {{i8\*|ptr}} align 8 %src, i{{.*}} 56, i1 false)
// CHECK-NOT: call void @llvm.memcpy

// CHECK-NOT: call void @llvm.memcpy
33 changes: 0 additions & 33 deletions tests/codegen/mem-replace-direct-memcpy.rs

This file was deleted.

34 changes: 34 additions & 0 deletions tests/codegen/mem-replace-simple-type.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// compile-flags: -O -C no-prepopulate-passes
// min-llvm-version: 15.0 (for opaque pointers)
// only-x86_64 (to not worry about usize differing)
// ignore-debug (the debug assertions get in the way)

#![crate_type = "lib"]

#[no_mangle]
// CHECK-LABEL: @replace_usize(
pub fn replace_usize(r: &mut usize, v: usize) -> usize {
// CHECK-NOT: alloca
// CHECK: %[[R:.+]] = load i64, ptr %r
// CHECK: store i64 %v, ptr %r
// CHECK: ret i64 %[[R]]
std::mem::replace(r, v)
}

#[no_mangle]
// CHECK-LABEL: @replace_ref_str(
pub fn replace_ref_str<'a>(r: &mut &'a str, v: &'a str) -> &'a str {
// CHECK-NOT: alloca
// CHECK: %[[A:.+]] = load ptr
// CHECK: %[[B:.+]] = load i64
// CHECK-NOT: store
// CHECK-NOT: load
// CHECK: store ptr
// CHECK: store i64
// CHECK-NOT: load
// CHECK-NOT: store
// CHECK: %[[P1:.+]] = insertvalue { ptr, i64 } poison, ptr %[[A]], 0
// CHECK: %[[P2:.+]] = insertvalue { ptr, i64 } %[[P1]], i64 %[[B]], 1
// CHECK: ret { ptr, i64 } %[[P2]]
std::mem::replace(r, v)
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
_4 = &raw const (*_1); // scope 1 at $DIR/lower_intrinsics.rs:+2:55: +2:56
- _3 = option_payload_ptr::<usize>(move _4) -> [return: bb1, unwind unreachable]; // scope 1 at $DIR/lower_intrinsics.rs:+2:18: +2:57
- // mir::Constant
- // + span: $DIR/lower_intrinsics.rs:132:18: 132:54
- // + span: $DIR/lower_intrinsics.rs:137:18: 137:54
- // + literal: Const { ty: unsafe extern "rust-intrinsic" fn(*const Option<usize>) -> *const usize {option_payload_ptr::<usize>}, val: Value(<ZST>) }
+ _3 = &raw const (((*_4) as Some).0: usize); // scope 1 at $DIR/lower_intrinsics.rs:+2:18: +2:57
+ goto -> bb1; // scope 1 at $DIR/lower_intrinsics.rs:+2:18: +2:57
Expand All @@ -37,7 +37,7 @@
_6 = &raw const (*_2); // scope 2 at $DIR/lower_intrinsics.rs:+3:55: +3:56
- _5 = option_payload_ptr::<String>(move _6) -> [return: bb2, unwind unreachable]; // scope 2 at $DIR/lower_intrinsics.rs:+3:18: +3:57
- // mir::Constant
- // + span: $DIR/lower_intrinsics.rs:133:18: 133:54
- // + span: $DIR/lower_intrinsics.rs:138:18: 138:54
- // + literal: Const { ty: unsafe extern "rust-intrinsic" fn(*const Option<String>) -> *const String {option_payload_ptr::<String>}, val: Value(<ZST>) }
+ _5 = &raw const (((*_6) as Some).0: std::string::String); // scope 2 at $DIR/lower_intrinsics.rs:+3:18: +3:57
+ goto -> bb2; // scope 2 at $DIR/lower_intrinsics.rs:+3:18: +3:57
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
_4 = _2; // scope 0 at $DIR/lower_intrinsics.rs:+1:33: +1:34
- _0 = offset::<*const i32, isize>(move _3, move _4) -> [return: bb1, unwind unreachable]; // scope 0 at $DIR/lower_intrinsics.rs:+1:5: +1:35
- // mir::Constant
- // + span: $DIR/lower_intrinsics.rs:139:5: 139:29
- // + span: $DIR/lower_intrinsics.rs:144:5: 144:29
- // + literal: Const { ty: unsafe extern "rust-intrinsic" fn(*const i32, isize) -> *const i32 {offset::<*const i32, isize>}, val: Value(<ZST>) }
+ _0 = Offset(move _3, move _4); // scope 0 at $DIR/lower_intrinsics.rs:+1:5: +1:35
+ goto -> bb1; // scope 0 at $DIR/lower_intrinsics.rs:+1:5: +1:35
Expand Down
5 changes: 5 additions & 0 deletions tests/mir-opt/lower_intrinsics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ pub fn read_via_copy_uninhabited(r: &Never) -> Never {
unsafe { core::intrinsics::read_via_copy(r) }
}

// EMIT_MIR lower_intrinsics.write_via_move_string.LowerIntrinsics.diff
pub fn write_via_move_string(r: &mut String, v: String) {
unsafe { core::intrinsics::write_via_move(r, v) }
}

pub enum Never {}

// EMIT_MIR lower_intrinsics.option_payload.LowerIntrinsics.diff
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
- // MIR for `write_via_move_string` before LowerIntrinsics
+ // MIR for `write_via_move_string` after LowerIntrinsics

fn write_via_move_string(_1: &mut String, _2: String) -> () {
debug r => _1; // in scope 0 at $DIR/lower_intrinsics.rs:+0:30: +0:31
debug v => _2; // in scope 0 at $DIR/lower_intrinsics.rs:+0:46: +0:47
let mut _0: (); // return place in scope 0 at $DIR/lower_intrinsics.rs:+0:57: +0:57
let mut _3: *mut std::string::String; // in scope 0 at $DIR/lower_intrinsics.rs:+1:47: +1:48
let mut _4: std::string::String; // in scope 0 at $DIR/lower_intrinsics.rs:+1:50: +1:51
scope 1 {
}

bb0: {
StorageLive(_3); // scope 1 at $DIR/lower_intrinsics.rs:+1:47: +1:48
_3 = &raw mut (*_1); // scope 1 at $DIR/lower_intrinsics.rs:+1:47: +1:48
StorageLive(_4); // scope 1 at $DIR/lower_intrinsics.rs:+1:50: +1:51
_4 = move _2; // scope 1 at $DIR/lower_intrinsics.rs:+1:50: +1:51
- _0 = write_via_move::<String>(move _3, move _4) -> [return: bb1, unwind unreachable]; // scope 1 at $DIR/lower_intrinsics.rs:+1:14: +1:52
- // mir::Constant
- // + span: $DIR/lower_intrinsics.rs:129:14: 129:46
- // + literal: Const { ty: unsafe extern "rust-intrinsic" fn(*mut String, String) {write_via_move::<String>}, val: Value(<ZST>) }
+ (*_3) = move _4; // scope 1 at $DIR/lower_intrinsics.rs:+1:14: +1:52
+ goto -> bb1; // scope 1 at $DIR/lower_intrinsics.rs:+1:14: +1:52
}

bb1: {
StorageDead(_4); // scope 1 at $DIR/lower_intrinsics.rs:+1:51: +1:52
StorageDead(_3); // scope 1 at $DIR/lower_intrinsics.rs:+1:51: +1:52
goto -> bb2; // scope 0 at $DIR/lower_intrinsics.rs:+2:1: +2:2
}

bb2: {
return; // scope 0 at $DIR/lower_intrinsics.rs:+2:2: +2:2
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// MIR for `manual_replace` after PreCodegen

fn manual_replace(_1: &mut u32, _2: u32) -> u32 {
debug r => _1; // in scope 0 at $DIR/mem_replace.rs:+0:23: +0:24
debug v => _2; // in scope 0 at $DIR/mem_replace.rs:+0:36: +0:37
let mut _0: u32; // return place in scope 0 at $DIR/mem_replace.rs:+1:9: +1:13
scope 1 {
debug temp => _0; // in scope 1 at $DIR/mem_replace.rs:+1:9: +1:13
}

bb0: {
_0 = (*_1); // scope 0 at $DIR/mem_replace.rs:+1:16: +1:18
(*_1) = _2; // scope 1 at $DIR/mem_replace.rs:+2:5: +2:11
return; // scope 0 at $DIR/mem_replace.rs:+4:2: +4:2
}
}
Loading