Let's say we have the following function:
pub fn eq(a: &mut u32, b: &u32) -> bool {
core::ptr::eq(a, b)
}
I would expect for it to get optimized to always return false
, but it does not happen in practice (see Compiler Explorer ). Is it just a missed optimization in LLVM or is there another reason for that?
3 Likes
kornel
April 15, 2025, 2:39pm
2
For the record, the IR of the (a: &mut u32, b: &mut u32)
version is:
define noundef zeroext i1 @eq(ptr noalias noundef readnone align 4 dereferenceable(4) %a, ptr noalias noundef readnone align 4 dereferenceable(4) %b) unnamed_addr {
start:
%_0 = icmp eq ptr %a, %b
ret i1 %_0
}
So it seems that LLVM should be able to tell that noalias
pointers don't alias.
When the pointers are taken from local variables, it optimizes to false
directly.
1 Like
kornel
April 15, 2025, 2:56pm
3
However, there is a deep rabbit hole of pointer equality and mismatches between semantics of llvm.lifetime.start/end
and MIR's StorageLive/Dead
semantics.
LLVM wants to be able to hardcode not-equal for addresses of local variables, because that's always true in the C abstract machine, but handling of alloca
, loops, and stack-reusing optimizations on the real hardware can make local variables share an address, so you can get paradoxes where pointers are both equal and not equal.
3 Likes
CAD97
April 15, 2025, 10:31pm
4
Except that it's the exact same IR for fn(&u32, &u32)
; LLVM noalias
(and C restrict
) is only concerned with if the pointee is modified. MIR semantics would permit optimizing the comparison to false
, but the currently emitted LLVM semantics don't.
2 Likes
Could this optimization cause problems with ZSTs sharing an address?
1 Like
kornel
April 16, 2025, 2:15pm
6
ZSTs already don't have meaningful addresses. [(); !0]
has all items at the same address. Except maybe static ZSTs .
More on pointer equality pitfalls:
opened 09:38PM - 20 Sep 19 UTC
C-open-question
S-pending-design
A-pointer-equality
References to constants are not guaranteed to have unique addresses:
```rust
a… ssert!(&2 as *const i32 != &2 as *const i32); // fails
```
Since `const`s are just aliases, the same holds for those:
```rust
const A: i32 = 2;
const B: i32 = 2;
assert!(&A as *const i32 != &B as *const i32); // fails
```
What about `static`s? `static` variables with interior mutability (and `static mut` variables) obviously must have unique addresses, but what about ones without?
```rust
static A: i32 = 2;
static B: i32 = 2;
assert!(&A as *const i32 != &B as *const i32); // passes
```
And local variables? (Assuming that both variables are alive at the point of comparison, since obviously variables that have fallen out of scope can have their addresses reused.)
```rust
let a = 2;
let b = 2;
assert!(&a as *const i32 != &b as *const i32); // passes
```
Currently, rustc seems to produce unique addresses in both cases. But @gnzlbg [is under the impression](https://github.com/rust-lang/unsafe-code-guidelines/issues/205#issuecomment-532665149) that multiple local variables are not guaranteed to have distinct addresses.
Address uniqueness can be a useful property, e.g. if you want a unique 'sentinel' value to assign to a pointer variable. On the other hand, I'd say Rust usually avoids giving much significance to something being a variable as opposed to an expression.
A related issue is #15, which is about whether the address of something can change over time.
### Compared to C and C++
In C, rvalues are not implicitly bound to addresses unless assigned to a variable (or a C99 compound literal). [C appears to guarantee that distinct variables have distinct addresses.](https://stackoverflow.com/questions/6236762/in-c-are-const-variables-guaranteed-to-be-distinct-in-memory/6237094#6237094)
In C++, rvalues can be implicitly bound to const references, which gives them an address: this is "temporary materialization" and creates a "temporary object". Like C, the C++ spec guarantees that distinct "objects" "compare unequal", so I think this assertion is guaranteed to pass (not sure though):
```cpp
#include <assert.h>
void foo(const int &a, const int &b) {
assert(&a != &b);
}
int main() {
foo(2, 2);
}
```
In practice, this means that the compiler always stores a copy of the constant on the stack and takes the address of that, rather than directly referencing a static allocation.