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

ptr::read unable to assume slice is always not null #106369

Closed
AnsonYeung opened this issue Jan 2, 2023 · 7 comments · Fixed by #109035
Closed

ptr::read unable to assume slice is always not null #106369

AnsonYeung opened this issue Jan 2, 2023 · 7 comments · Fixed by #109035
Labels
A-codegen Area: Code generation A-LLVM Area: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues. I-slow Issue: Problems and improvements with respect to performance of generated code. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.

Comments

@AnsonYeung
Copy link

Expects the following function to always return true since it's undefined behaviour otherwise.

use std::ptr;

pub unsafe fn test(s: &Vec<&[i32]>, idx: usize) -> bool {
    unsafe { Some(ptr::read(s.as_ptr().add(idx))).is_some() }
}

Actual result:

example::test:
        mov     rax, qword ptr [rdi + 8]
        shl     rsi, 4
        cmp     qword ptr [rax + rsi], 0
        setne   al
        ret

https://rust.godbolt.org/z/xzcTr15Yj
Causes #106368

@AnsonYeung AnsonYeung changed the title Compiler unable to assume slice is always not null ptr::read unable to assume slice is always not null Jan 2, 2023
@AnsonYeung
Copy link
Author

AnsonYeung commented Jan 2, 2023

This is related to the function ptr::read, replacing it with deref produces the expected result.

use std::ptr;

pub unsafe fn test_ptr_read(s: *const &[i32]) -> bool {
    let x: &[i32] = unsafe { ptr::read(s) };
    Some(x).is_some()
}

pub unsafe fn test_deref(s: *const &[i32]) -> bool {
    let x: &[i32] = unsafe { *s };
    Some(x).is_some()
}

pub fn test_expected(s: &[i32]) -> bool {
    Some(s).is_some()
}
example::test_ptr_read:
        cmp     qword ptr [rdi], 0
        setne   al
        ret

example::test_deref:
        mov     al, 1
        ret

example::test_expected:
        mov     al, 1
        ret

https://rust.godbolt.org/z/z3bKfac6s

@saethlin
Copy link
Member

saethlin commented Jan 2, 2023

This looks a lot like #93011 to me. ptr::read does a copy_nonoverlapping into a MaybeUninit, perhaps that wrapping also causes LLVM to lose the nonnull.

@Noratrieb
Copy link
Member

This can be minimized even further to just this

#[no_mangle]
pub unsafe fn bad(ptr: *const &i32) -> bool {
    unsafe { Some(ptr::read(ptr)).is_some() }
}

which on master emits (rustc read.rs --crate-type=lib -Copt-level=3 --emit=asm)

bad:
	.cfi_startproc
	cmpq	$0, (%rdi)
	setne	%al
	retq

This actually gets fixed by applying

diff --git a/compiler/rustc_codegen_llvm/src/abi.rs b/compiler/rustc_codegen_llvm/src/abi.rs
index a6fd2a7de6b..30006ac5e7b 100644
--- a/compiler/rustc_codegen_llvm/src/abi.rs
+++ b/compiler/rustc_codegen_llvm/src/abi.rs
@@ -81,7 +81,6 @@ fn should_use_mutable_noalias(cx: &CodegenCx<'_, '_>) -> bool {
             } else {
                 attrs.push(llvm::CreateDereferenceableOrNullAttr(cx.llcx, deref));
             }
-            regular -= ArgAttribute::NonNull;
         }
         for (attr, llattr) in OPTIMIZATION_ATTRIBUTES {
             if regular.contains(attr) {

which is weird, because this shouldn't change anything as dereferenceable implies nonnull (at least as far as I know and opts seem to rely on this).

With this applied, I now get

bad:
	.cfi_startproc
	movb	$1, %al
	retq

which is good.

But, this still doesn't help with the original example in the issue, which is the same with and without the patch.

@Noratrieb Noratrieb added A-LLVM Area: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues. A-codegen Area: Code generation I-slow Issue: Problems and improvements with respect to performance of generated code. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. labels Jan 2, 2023
@clubby789
Copy link
Contributor

clubby789 commented Jan 2, 2023

If we add a !nonnull when the slice pointer is loaded then the codegen (-Copt-level=3 -Cdebuginfo=0 -Cno-prepopulate-passes ) for this case looks better (godbolt). The IR before the generated assume isn't removed, but it codegens to just a mov al, 1; ret

pub unsafe fn test_ptr_read(s: *const &[()]) -> bool {
    let x: &[()] = unsafe { std::ptr::read(s) };
    Some(x).is_some()
}

@clubby789
Copy link
Contributor

It looks like the problem isn't the CopyNonOverlapping - rather, when building the load, the layout of MaybeUninit (used in ptr::read) is used, (even though we're loading from an assume_init), so we don't know of any niches. godbolt

@AnsonYeung
Copy link
Author

AnsonYeung commented Jan 3, 2023

After some more digging it seems the issue is reported back in 2020 in #73258 . Seems like if it is imported internally to the crate, it can realize the optimization for reference but not slice

pub unsafe fn indirect_read<T>(src: *const T) -> T {
    std::ptr::read(src)
}

pub unsafe fn test_ptr_read(s: *const &[i32]) -> bool {
    let x: &[i32] = unsafe { indirect_read(s) };
    Some(x).is_some()
}

https://godbolt.org/z/MTWEbsvvc

@scottmcm
Copy link
Member

scottmcm commented Mar 15, 2023

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-codegen Area: Code generation A-LLVM Area: Code generation parts specific to LLVM. Both correctness bugs and optimization-related issues. I-slow Issue: Problems and improvements with respect to performance of generated code. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants