Declare two variable both i32 1

fn main() {
    let i1 = &1;
    let i2 = &1;
    if i1 == i2 {
        println!("immutable borrow is the same reference");
    }
}

so, there is only one i32 and i1 and i2 is the same reference, they both reference to the same 1?
is there any rust document explain this?

References and borrowing are covered in The Book:

1 Like

== compares using partial_eq which takes both parameters by reference, even though it compares the values behind those references. So the code you posted is comparing integer values, not addresses.

It is possible to compare reference addresses in Rust with ptr::eq but this is almost always meaningless never useful. Addresses may not even exist at runtime, because values may be placed in registers in the optimized code. EDIT: Comparing addresses can be meaningful when working with raw pointers, but even that is rarely needed.

5 Likes

Note that == will implicitly add an additional reference to the two sides when calling the PartialEq impl, so it will indeed use the impl for &i32. However references are supposed to behave more like handles to the value they reference rather than pointers, and for this reason their implementations of various traits will use the pointed value (e.g. PartialEq will compare the pointed value, Display will print the pointed value etc etc)

5 Likes

Comparing &i32 == &i32 delegates to comparing i32 == i32 because this impl does that

impl<A, B> PartialEq<&B> for &A
where
    A: PartialEq<B> + ?Sized,
    B: ?Sized,

AFAICT this behavior isn't actually documented. I was certainly surprised too when I first learned this.

2 Likes

It’s say it generally often is pretty meaningful, in the sense of giving sensible results that look like everything always had it’s own little, reliably stable address in stack memory. However true “addresses may not even exist at runtime” is, it’s generally referring to a compiler optimization that does take into account that an address isn’t meaningfully observed.

So this:

fn one() -> i32 { 1 }

fn main() {
    let i1 = &one();
    let i2 = &one();
    if std::ptr::eq(i1, i2) {
        println!("immutable borrow is the same reference");
    } else {
        println!("different addresses: {i1:p}, {i2:p}");
    }
}

will typically[1] report the addresses to be different, even with all optimizations turned on (nonetheless, the compiled code might very reasonably never be putting any actual data in these locations if it still knows that it would be UB for any other part of the program to read from these, anyway…

…though now that I’ve passed those references on to println which is full of dynamic code (not inlined) that the compiler tends to refuse to reason about, it seems like it might probably really have to put data there on the stack now – possibly more representative could be something like

println!("different addresses: 0x{:x}, 0x{:x}", <*const _>::addr(i1), <*const _>::addr(i2));

(which still reports different addresses even in release mode).


Now, &1 however is subject to rvalue static promotion (1, 2), so the address actually can be (and tends to be) the same between those.


  1. or maybe even is guaranteed to? ↩︎

2 Likes

Rust doesn't use addresses for object identity. Objects aren't implicitly compared by address. == doesn't compare references as pointers.

Addresses of variables and temporary values are meaningless, and can change at any time due to moves, which includes assignments and argument passing.

Addresses will also change depending on what optimisations are applied. It's possible that values/variables have no address at all. It's possible that & does not generate any code, and there's no address for it. In your example, after optimizations in the --release mode, the i1 won't exist, i2 won't exist, the if won't exist either, there won't be any 1, and there won't be any addresses involved. The code will unconditionally run println!.

Even if you force Rust to show you some address of a variable, it may create a temporary junk address just for your comparison, and then never use it again.

3 Likes

I believe this is false. I haven't found anything in the reference that says this one way or the other, but previous discussions on the same claim failed to provide any example code with the address changing, or any statement to this effect in documentation.

I am pretty sure the rules are the same as in C and C++: addresses of local variables or heap-allocated objects don't spontaneously change.

This is talking about what happens in compiled machine code, not about the semantics of the original Rust code. The Rust semantics are (I think) that a variable never changes its address, but the code may be compiled to totally different machine code that moves things in memory behind the scenes, does completely different arithmetic operations behind the scenes, etc, as long as the observed behavior is identical to the original Rust. This rule is exactly same as in C or C++.

You're missing two points:

The general compiler "Golden rule" is that everything is "as-if" it were following the language rules. That is, there are zero guarantees about "what happens in compiled machine code" that are not guarantees about "semantics of the original Rust code" (this is the same in any language I've ever seen above assembly - the only difference is how detailed that specification is).

The actual value of the address is meaningless specifically in that it may not even exist, or change value during execution if not observed, and further that the observed address doesn't need to have any particular relationship with the "actual" value, whatever that means. If you don't believe this, try comparing the assembly in godbolt with optimizations with and without printing or comparing the addresses.

It's best to consider a reference to a value as what it actually is: a different value, that includes semantics such as being able to be dereferenced to produce a place, which, like any other set of operations, can affect the output in arbitrarily wide-reaching ways (up to the final linked output, at least)

1 Like

I did not miss this point. On the contrary, this is exactly what I was trying to explain in my post.

In order to even talk about the "as if" rule, you first have to specify what the semantics of the original language is. After you specify the semantics, the as-if rule says the compiler can change the code any way it wants as long as the observed behavior is same.

So in this case, the question is: is Rust semantics such that variables have constant address, or that they may change? I believe they have constant addresses (although it's hard to prove for sure because we don't have a formal specification).

Hence, the compiler has to make sure that whatever the translated code does, the observed behavior is consistent with the original variables maintaining constant addresses.

Here is an analogy:

let a = b + c;

The Rust semantics is that this instruction adds b to c and puts the result in a.

The compiled machine code might not have the add instruction at all, it just has to behave identically, as if the two variables got added.

That doesn't mean we can say that in Rust "+ doesn't always add, it's meaningless to talk about + as if it was always an addition, it can do whatever it wants". No, "+" always adds, that's the semantics.

The fact that the assembly internally does something different doesn't contradict what I said. It's exactly what I tried to explain: the assembly code doesn't have to match semantics of Rust step by step, it just has to match the observed behavior of Rust semantics. Hence the fact that something unobserved happens internally in assembly does not prove anything about what the rules are in Rust.

Yes, but the semantics actually guaranteed by references and pointers are possibly much weaker (or rather, specific) than you might expect.

Most relevantly for this discussion, pointer equality doesn't consider provenance, which essentially means it is only meaningful as a way to consider if two pointers refer to "the same" object if they were both derived from the same "original pointer" (be that via (re)borrows or pointer arithmetic, etc) as the std::ptr docs put it (for locals, said "original pointer" is the "name", presumably meaning the resolved declaration rather than the literal identifier).

That means that this is guaranteed to return true:

let a = 1;
let x: *const _ = &a;
let y: *const _ = &a;
std::ptr::eq(x, y)

but the reverse is not the case: for example there's no guarantee that this does not return true:

let a = 1;
let b = 2;
let x: *const _ = &a;
let y: *const _ = &b;
std::ptr::eq(x, y)

That's part of what's meant by "addresses are meaningless", the other relevant part being that they are very optimizer sensitive and taking the address for debugging purposes will not tell you anything like "what's actually happening", a common beginner mistake. (it may also be used for the fact that Rust generally and fundamentally assumes that values can be moved, but presumably you're well aware of that already!)

Thank you. And that was my claim. The original claim by @kornel was:

And that's what I was disputing.

That's interesting but that's a different discussion and I made no claim about this.

Is this true though? I thought there's gotta be a rule that addresses of bytes of variables alive at the same time don't overlap. But perhaps there isn't? There is a statement that says: "The base address of an allocated object is not necessarily unique.", but I thought that maybe that is only relevant for ZSTs. According to the docs, a pointer is an address and a provenance, and provenance is a set of permissions to access a set of addresses. So if the two addresses are allowed to be same, wouldn't that mean that writing to one would modify the other (if these variables were mutable)?

I wasn't as much disputing whether or not they are "meaningless", I was disputing the claim that they "can change at any time".

Well then your getting into what it "actually means" for both "change" and "any time" which is dangerously philosophical, but in practical terms that could easily be true in either the sense that the value can be moved around by the optimizer (eg the current value is actually in a register while you're using it) while it knows you're not looking at it, or it could mean that what behavior you observe in one compilation could be completely different on a different compilation even if "nothing has changed" (for some presumably not literal value of "nothing")

Then we're back to the distinction: what happens in Rust vs what happens in translated machine code. There is no concept of registers in Rust, registers are a concept from a different language -- machine code.

It's as if somebody asked "what value gets stored in a in the following code"?

let a = 2i32.pow(5);

and you came back and said: "You never know. Maybe the optimizer decides to put 33 rather than 32 in a register after optimization, because it's easier for it to store the value incremented by 1".

All such questions are about the semantics of Rust, not about how the optimizer is going to modify the code in unobservable ways behind the scenes, because obviously it can modify the code any which way it wants as long as it's not observable.

@kornel has just edited the claim under contention to this:

But it still is false.

When you have a variable x, then:

  • when you assign something to x, the address of the variable x doesn't change
  • when you assign x to y, the address of the variable x also doesn't change; y is a different variable
  • when you move x to y, the address of the variable x still doesn't change, y is a different variable
  • when you pass x as an argument to a function, the address of the variable x still doesn't change, the argument is a different variable

The address of a variable never changes, it's the same throughout the whole life of the variable.

(edit: note this was written before your last reply)

Well yes, but that's the getting towards philosophy part: what is someone talking about when they refer to an "address" or at "any time" - you're referring to (perhaps?) "the address you would observe if you created a pointer to a value and compared or printed it", others, including newcomers like the OP, often are not.

Beside the point really, but in thinking about this I'm not even sure it's actually illegal for rust to have &x as *const _ as usize give different values every time so long as it can also update other values consistently enough that comparisons still work; eg if some monster figures out a "Rust#" or the like, a compacting GC could easily behave that way.

I don't think that makes sense. What would it even mean to "update other values consistently"? What if you had already printed the usize to the screen? You can't change that.

Or do you mean "in cases where nobody will notice"? But then that's semantically no different from saying the new values were there from the very beginning, so there is no point in saying they can change.

Yes, even after printing the value to the screen it may be legal to change the value. I'm very non confident of that, to be clear! It would imply invented writes, but those may be technically legal if they're coming from the AM itself.

I don't think that's the intent. If this were the case, then for example the function with_addr that converts a usize back to a pointer would be useless.

My thinking is that if the lowering of the abstract machine has some mechanism that can move around values like the dotnet runtime, that it would also need to update not only pointers to said values but possibly also addresses that would be moved back to being a pointer with that (with the model that "provenance" in such a machine, like most machines, is a ethereal "permission" to use and compare pointers, not a strict value)