Pointer comparisons

rustc 1.68 compiles both of the functions below to pointer comparisons:

pub fn compare(a: &mut [i32], b: &mut [i32]) -> bool {
    a.as_ptr() == b.as_ptr()
}

pub fn compare_2(a: &mut i32, b: &mut i32) -> bool {
    (a as *mut i32) == (b as *mut i32)
}

That is, both compile on x86 to

cmp rdi, rsi
sete al
ret

Can either of these be optimized to just return false? For the first function, we can construct a zero-length slice from NonNull::dangling, and we can produce two slices that have the same pointer. But can rustc ignore that fact?

Is NonNull::<T>::dangling guaranteed to always return a pointer whose numeric value is the alignment of T?

No. You can create two empty slices at the same offset of some nonzero slice. You don't need dangling.

The second one (always pointing at some memory) , should be possible.

Yes.

You are probably confusing "overlap" with "same address". In Rust, two regions overlap if and only if they have a (non-empty) memory region that is part of both allocations.

Mutable references aren't allowed to overlap, but they can have the same address if at least one of them is zero-sized, because a zero-sized value never overlaps with anything (since the length of a potential overlap would always be zero).

4 Likes

:smiling_face_with_tear: LLVM suprisingly won't optimize this for some reason. (not sure if I missed something...)

define noundef zeroext i1 @ptr_eq(
    ptr noundef noalias %a,
    ptr noundef noalias %b
) unnamed_addr {
start:
  %0 = icmp eq ptr %a, %b
  ret i1 %0
}

Compiler Exploror

1 Like

noalias isn't strong enough to allow it to do this.

From https://llvm.org/docs/LangRef.html#parameter-attributes:

This indicates that memory locations accessed via pointer values based on the argument or return value are not also accessed, during the execution of the function, via pointer values not based on the argument or return value.

Since that's about accessed, it doesn't mean that the pointers aren't the same, because if it's not accessed at all, then it would meet the requirements for what LLVM knows about it. (As I understand it, at least.)

3 Likes

Interesting to know. But following code still won't return false directly.

pub fn mut_ptr_eq(a: &mut i32, b: &mut i32) -> bool {
  *a = 0;
  *b = 2;
  // mov     dword ptr [rsi], 2
  *a = *a + 1;
  // ; LLVM figured out assignment to `*b` won't affect `*a`
  // mov     dword ptr [rdi], 1
  a as *mut i32 == b as *mut i32
  // ; But we still got a `cmp`
  // cmp     rdi, rsi
  // sete    al
  // ret
}

Thanks for the explanation. What I want to understand is this: if you have a reference to a zero-sized type, why isn't the compiler allowed to treat its underlying pointer as an arbitrary value?

I don't understand the question. The addresses of two zero-sized objects may be the same or different. Thus the compiler can't say with certainty that they will or will not be equal. They may or may not be equal.

I believe we allow relying on references to ZSTs have stable address. (The fact a reference to ZST is not a ZST itself says something.)

That's interesting. Does that guarantee facilitate something? (I.e., is there something, in, say, tokio, that requires references to ZSTs to have stable addresses?) Does allocating a ZST give you a "new" address?

I mean, would you expect the following code to fail the assertion?

let p: &() = unsafe { &*(13 as *const ()) };
let q: &() = unsafe { &*(37 as *const ()) };
assert!(core::ptr::eq(p, q) == false);

I would find it very upsetting if the compiler were allowed to turn this code into a failing assert.

3 Likes

Or this one.

1 Like

They can be stable (i.e. an object you are referencing can't magically move around in memory while they are alive), but you can't assume they will always be the same.

Here is one trivial counter-example:

struct HasZst {
    first: u32,
    zst: (),
    second: String,
}

fn main() {
    let on_the_stack = ();
    let object_with_zst = HasZst {
        first: 42,
        zst: (),
        second: String::new(),
    };

    println!("{:p} {:p}", &on_the_stack, &object_with_zst.zst);
    assert!(std::ptr::eq(&on_the_stack, &object_with_zst.zst));
}

(playground)

Output when running in release mode:

---- Standard Error ----

   Compiling playground v0.0.1 (/playground)
...
    Finished release [optimized] target(s) in 0.57s
     Running `target/release/playground`
thread 'main' panicked at 'assertion failed: std::ptr::eq(&on_the_stack, &object_with_zst.zst)', src/main.rs:16:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

---- Standard Output ----

0x7ffead958b30 0x7ffead958b38
1 Like

I see; thank you for the example.